react-doctor 0.9.3 → 0.9.4-dev.25dbf6d

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
@@ -9,7 +9,7 @@ import { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FIL
9
9
  import * as Crypto from "node:crypto";
10
10
  import crypto, { createHash } from "node:crypto";
11
11
  import * as NodeChildProcess from "node:child_process";
12
- import { execFileSync, spawn, spawnSync } from "node:child_process";
12
+ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
13
13
  import { ResolverFactory } from "oxc-resolver";
14
14
  import ts from "typescript";
15
15
  import * as NodeUrl from "node:url";
@@ -3647,7 +3647,7 @@ const flatMap$5 = /* @__PURE__ */ dual(2, (self, f) => isNone(self) ? none() : f
3647
3647
  * @category filtering
3648
3648
  * @since 2.0.0
3649
3649
  */
3650
- const filter = /* @__PURE__ */ dual(2, (self, predicate) => isNone(self) ? none() : predicate(self.value) ? some(self.value) : none());
3650
+ const filter$1 = /* @__PURE__ */ dual(2, (self, predicate) => isNone(self) ? none() : predicate(self.value) ? some(self.value) : none());
3651
3651
  //#endregion
3652
3652
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Result.js
3653
3653
  /**
@@ -7275,6 +7275,16 @@ const catchTag$2 = /* @__PURE__ */ dual((args) => isEffect$1(args[0]), (self, k,
7275
7275
  return catchIf$2(self, Array.isArray(k) ? (e) => hasProperty(e, "_tag") && k.includes(e._tag) : isTagged(k), f, orElse);
7276
7276
  });
7277
7277
  /** @internal */
7278
+ const catchReasons$1 = /* @__PURE__ */ dual((args) => isEffect$1(args[0]), (self, errorTag, cases, orElse) => {
7279
+ let keys;
7280
+ return catchIf$2(self, (e) => isTagged(e, errorTag) && hasProperty(e, "reason") && hasProperty(e.reason, "_tag") && isString(e.reason._tag), (e) => {
7281
+ const reason = e.reason;
7282
+ keys ??= Object.keys(cases);
7283
+ if (keys.includes(reason._tag)) return internalCall(() => cases[reason._tag](reason, e));
7284
+ return orElse ? internalCall(() => orElse(reason, e)) : fail$7(e);
7285
+ });
7286
+ });
7287
+ /** @internal */
7278
7288
  const mapError = /* @__PURE__ */ dual(2, (self, f) => catch_$2(self, (error) => failSync(() => f(error))));
7279
7289
  const mapBoth = /* @__PURE__ */ dual(2, (self, options) => matchEffect$2(self, {
7280
7290
  onFailure: (e) => failSync(() => options.onFailure(e)),
@@ -12184,6 +12194,42 @@ const catch_$1 = catch_$2;
12184
12194
  */
12185
12195
  const catchTag$1 = catchTag$2;
12186
12196
  /**
12197
+ * Catches multiple reasons within a tagged error using an object of handlers.
12198
+ *
12199
+ * **Example** (Handling multiple error reasons)
12200
+ *
12201
+ * ```ts
12202
+ * import { Data, Effect } from "effect"
12203
+ *
12204
+ * class RateLimitError extends Data.TaggedError("RateLimitError")<{
12205
+ * retryAfter: number
12206
+ * }> {}
12207
+ *
12208
+ * class QuotaExceededError extends Data.TaggedError("QuotaExceededError")<{
12209
+ * limit: number
12210
+ * }> {}
12211
+ *
12212
+ * class AiError extends Data.TaggedError("AiError")<{
12213
+ * reason: RateLimitError | QuotaExceededError
12214
+ * }> {}
12215
+ *
12216
+ * declare const program: Effect.Effect<string, AiError>
12217
+ *
12218
+ * const handled = program.pipe(
12219
+ * Effect.catchReasons("AiError", {
12220
+ * RateLimitError: (reason) =>
12221
+ * Effect.succeed(`Retry after ${reason.retryAfter}s`),
12222
+ * QuotaExceededError: (reason) =>
12223
+ * Effect.succeed(`Quota exceeded: ${reason.limit}`)
12224
+ * })
12225
+ * )
12226
+ * ```
12227
+ *
12228
+ * @category error handling
12229
+ * @since 4.0.0
12230
+ */
12231
+ const catchReasons = catchReasons$1;
12232
+ /**
12187
12233
  * Handles both recoverable and unrecoverable errors by providing a recovery
12188
12234
  * effect.
12189
12235
  *
@@ -15586,7 +15632,7 @@ function transformOptional(f) {
15586
15632
  */
15587
15633
  function withDefault$1(defaultValue) {
15588
15634
  return new Getter((o) => {
15589
- const filtered = filter(o, isNotUndefined);
15635
+ const filtered = filter$1(o, isNotUndefined);
15590
15636
  return isSome(filtered) ? succeed$2(filtered) : mapEager(defaultValue, some);
15591
15637
  });
15592
15638
  }
@@ -19434,6 +19480,10 @@ var JsonReportProjectEntryV3 = class extends Class("JsonReportProjectEntryV3")({
19434
19480
  scannedFileCount: optional(Number$1),
19435
19481
  elapsedMilliseconds: Number$1
19436
19482
  }) {};
19483
+ var JsonReportSkippedProject = class extends Class("JsonReportSkippedProject")({
19484
+ directory: String$1,
19485
+ reason: Literal("max-duration")
19486
+ }) {};
19437
19487
  /** Original full, diff, and staged report contract. */
19438
19488
  var JsonReportV1 = class extends Class("JsonReportV1")({
19439
19489
  schemaVersion: Literal(1),
@@ -19462,6 +19512,7 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
19462
19512
  reactDetected: optional(Boolean$1),
19463
19513
  diff: NullOr(JsonReportDiffInfo),
19464
19514
  projects: ArraySchema(JsonReportProjectEntry),
19515
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19465
19516
  diagnostics: ArraySchema(Diagnostic),
19466
19517
  summary: JsonReportSummary,
19467
19518
  elapsedMilliseconds: Number$1,
@@ -19490,6 +19541,7 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
19490
19541
  diff: NullOr(JsonReportDiffInfo),
19491
19542
  baseline: JsonReportBaseline,
19492
19543
  projects: ArraySchema(JsonReportProjectEntry),
19544
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19493
19545
  diagnostics: ArraySchema(Diagnostic),
19494
19546
  summary: JsonReportSummary,
19495
19547
  elapsedMilliseconds: Number$1,
@@ -19506,6 +19558,7 @@ var JsonReportV3 = class extends Class("JsonReportV3")({
19506
19558
  diff: NullOr(JsonReportDiffInfo),
19507
19559
  baseline: optional(JsonReportBaseline),
19508
19560
  projects: ArraySchema(JsonReportProjectEntryV3),
19561
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19509
19562
  diagnostics: ArraySchema(JsonReportDiagnosticV3),
19510
19563
  summary: JsonReportSummary,
19511
19564
  elapsedMilliseconds: Number$1,
@@ -53488,6 +53541,47 @@ const flattenArray = (self) => transformPull$1(self, (pull) => {
53488
53541
  }));
53489
53542
  });
53490
53543
  /**
53544
+ * Filters arrays of elements emitted by a channel, applying the filter
53545
+ * to each element within the arrays and only emitting non-empty filtered arrays.
53546
+ *
53547
+ * **Example** (Filtering array output)
53548
+ *
53549
+ * ```ts
53550
+ * import { Array, Channel } from "effect"
53551
+ *
53552
+ * const nonEmptyArrayPredicate = Array.isReadonlyArrayNonEmpty
53553
+ *
53554
+ * // Create a channel that outputs arrays of mixed data
53555
+ * const arrayChannel = Channel.fromIterable([
53556
+ * Array.make(1, 2, 3, 4, 5),
53557
+ * Array.make(6, 7, 8, 9, 10),
53558
+ * Array.make(11, 12, 13, 14, 15)
53559
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
53560
+ *
53561
+ * // Filter arrays to keep only even numbers
53562
+ * const evenArraysChannel = Channel.filterArray(arrayChannel, (n) => n % 2 === 0)
53563
+ * // Outputs: [2, 4], [6, 8, 10], [12, 14]
53564
+ * // Note: Only non-empty filtered arrays are emitted
53565
+ *
53566
+ * // Arrays that would become empty after filtering are discarded entirely
53567
+ * const oddChannel = Channel.fromIterable([
53568
+ * Array.make(1, 3, 5),
53569
+ * Array.make(2, 4),
53570
+ * Array.make(7, 9)
53571
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
53572
+ * const filteredOddChannel = Channel.filterArray(oddChannel, (n) => n % 2 === 0)
53573
+ * // Outputs: [2, 4] (the arrays [1,3,5] and [7,9] are discarded)
53574
+ * ```
53575
+ *
53576
+ * @category filtering
53577
+ * @since 4.0.0
53578
+ */
53579
+ const filterArray = /* @__PURE__ */ dual(2, (self, predicate) => transformPull$1(self, (pull) => succeed$2(flatMap$2(pull, function loop(arr) {
53580
+ const passes = [];
53581
+ for (let i = 0; i < arr.length; i++) if (predicate(arr[i])) passes.push(arr[i]);
53582
+ return isReadonlyArrayNonEmpty(passes) ? succeed$2(passes) : flatMap$2(pull, loop);
53583
+ }))));
53584
+ /**
53491
53585
  * Filters and maps each element inside emitted non-empty arrays using a
53492
53586
  * `Filter`.
53493
53587
  *
@@ -55125,6 +55219,30 @@ const concat = /* @__PURE__ */ dual(2, (self, that) => flatten(fromArray([self,
55125
55219
  */
55126
55220
  const merge$1 = /* @__PURE__ */ dual((args) => isStream(args[0]) && isStream(args[1]), (self, that, options) => fromChannel(merge$2(toChannel(self), toChannel(that), options)));
55127
55221
  /**
55222
+ * Filters a stream to the elements that satisfy a predicate.
55223
+ *
55224
+ * **Example** (Filtering stream values)
55225
+ *
55226
+ * ```ts
55227
+ * import { Console, Effect, Stream } from "effect"
55228
+ *
55229
+ * const program = Effect.gen(function*() {
55230
+ * const stream = Stream.make(1, 2, 3, 4).pipe(
55231
+ * Stream.filter((n) => n % 2 === 0)
55232
+ * )
55233
+ * const values = yield* Stream.runCollect(stream)
55234
+ * yield* Console.log(values)
55235
+ * })
55236
+ *
55237
+ * Effect.runPromise(program)
55238
+ * // Output: [ 2, 4 ]
55239
+ * ```
55240
+ *
55241
+ * @category filtering
55242
+ * @since 2.0.0
55243
+ */
55244
+ const filter = /* @__PURE__ */ dual(2, (self, predicate) => fromChannel(filterArray(toChannel(self), predicate)));
55245
+ /**
55128
55246
  * Filters and maps stream elements in one pass using a `Filter`.
55129
55247
  *
55130
55248
  * @category filtering
@@ -61908,6 +62026,7 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
61908
62026
  const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
61909
62027
  const MINIFIED_SNIFF_BYTES = 65536;
61910
62028
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
62029
+ const GIT_CHECK_ATTR_MAX_BUFFER_BYTES = GIT_LS_FILES_MAX_BUFFER_BYTES;
61911
62030
  const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
61912
62031
  const IGNORED_DIRECTORIES = new Set([
61913
62032
  ".angular",
@@ -62216,13 +62335,9 @@ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
62216
62335
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
62217
62336
  const ABORT_EXIT_CODES = new Set([134, 3221226505]);
62218
62337
  const OXLINT_OOM_RESCUE_BUDGET_MS = 6e4;
62219
- const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
62220
- const LINT_PHASE_TIMEOUT_MS = 3e5;
62221
- const SCAN_TOTAL_DEADLINE_MS = 9e5;
62222
62338
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
62223
62339
  const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
62224
62340
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
62225
- const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
62226
62341
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
62227
62342
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
62228
62343
  const REACT_SERVER_DOM_PACKAGES = [
@@ -62269,7 +62384,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
62269
62384
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
62270
62385
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
62271
62386
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
62272
- const CORE_PACKAGE_VERSION = "0.9.3";
62387
+ const CORE_PACKAGE_VERSION = "0.9.4";
62273
62388
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
62274
62389
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
62275
62390
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -62282,17 +62397,20 @@ const SUPPLY_CHAIN_IGNORED_PACKAGES = new Set(["next"]);
62282
62397
  const isErrnoException = (error) => error instanceof Error && "code" in error;
62283
62398
  const IGNORABLE_READDIR_ERROR_CODES = new Set([
62284
62399
  "EACCES",
62400
+ "EBUSY",
62285
62401
  "EPERM",
62286
62402
  "ENOENT",
62287
62403
  "ENOTDIR",
62288
62404
  "EINVAL",
62289
62405
  "ELOOP",
62290
- "ENAMETOOLONG"
62406
+ "ENAMETOOLONG",
62407
+ "ETIMEDOUT",
62408
+ "UNKNOWN"
62291
62409
  ]);
62292
62410
  const isIgnorableReaddirError = (error) => isErrnoException(error) && typeof error.code === "string" && IGNORABLE_READDIR_ERROR_CODES.has(error.code);
62293
62411
  const readDirectoryEntries = (directoryPath) => {
62294
62412
  try {
62295
- return NFS.readdirSync(directoryPath, { withFileTypes: true });
62413
+ return fs.readdirSync(directoryPath, { withFileTypes: true });
62296
62414
  } catch (error) {
62297
62415
  if (isIgnorableReaddirError(error)) return [];
62298
62416
  throw error;
@@ -62300,14 +62418,14 @@ const readDirectoryEntries = (directoryPath) => {
62300
62418
  };
62301
62419
  const isFile = (filePath) => {
62302
62420
  try {
62303
- return NFS.statSync(filePath).isFile();
62421
+ return fs.statSync(filePath).isFile();
62304
62422
  } catch {
62305
62423
  return false;
62306
62424
  }
62307
62425
  };
62308
62426
  const isDirectory = (directoryPath) => {
62309
62427
  try {
62310
- return NFS.statSync(directoryPath).isDirectory();
62428
+ return fs.statSync(directoryPath).isDirectory();
62311
62429
  } catch {
62312
62430
  return false;
62313
62431
  }
@@ -62317,6 +62435,66 @@ const isPlainObject = (value) => {
62317
62435
  const prototype = Object.getPrototypeOf(value);
62318
62436
  return prototype === null || prototype === Object.prototype;
62319
62437
  };
62438
+ const LINGUIST_ATTRIBUTES = ["linguist-generated", "linguist-vendored"];
62439
+ const DISABLED_ATTRIBUTE_VALUES = new Set([
62440
+ "unspecified",
62441
+ "unset",
62442
+ "false",
62443
+ "0",
62444
+ "off",
62445
+ "no"
62446
+ ]);
62447
+ const buildInput = (relativePaths) => `${relativePaths.join("\0")}\0`;
62448
+ const parseIgnoredPaths = (output) => {
62449
+ const ignoredPaths = /* @__PURE__ */ new Set();
62450
+ const fields = output.split("\0");
62451
+ for (let fieldIndex = 0; fieldIndex + 2 < fields.length; fieldIndex += 3) {
62452
+ const filePath = fields[fieldIndex];
62453
+ const value = fields[fieldIndex + 2]?.toLowerCase();
62454
+ if (filePath && value && !DISABLED_ATTRIBUTE_VALUES.has(value)) ignoredPaths.add(filePath);
62455
+ }
62456
+ return ignoredPaths;
62457
+ };
62458
+ const collectGitLinguistIgnoredPaths = (rootDirectory, relativePaths) => {
62459
+ if (relativePaths.length === 0) return /* @__PURE__ */ new Set();
62460
+ const result = spawnSync("git", [
62461
+ "check-attr",
62462
+ "-z",
62463
+ "--stdin",
62464
+ ...LINGUIST_ATTRIBUTES
62465
+ ], {
62466
+ cwd: rootDirectory,
62467
+ encoding: "utf-8",
62468
+ input: buildInput(relativePaths),
62469
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES
62470
+ });
62471
+ if (result.error || result.status !== 0) return /* @__PURE__ */ new Set();
62472
+ return parseIgnoredPaths(result.stdout);
62473
+ };
62474
+ const collectGitLinguistIgnoredPathsCooperative = (rootDirectory, relativePaths, signal) => {
62475
+ if (relativePaths.length === 0) return Promise.resolve(/* @__PURE__ */ new Set());
62476
+ return new Promise((resolve, reject) => {
62477
+ signal?.throwIfAborted();
62478
+ execFile("git", [
62479
+ "check-attr",
62480
+ "-z",
62481
+ "--stdin",
62482
+ ...LINGUIST_ATTRIBUTES
62483
+ ], {
62484
+ cwd: rootDirectory,
62485
+ encoding: "utf-8",
62486
+ killSignal: "SIGKILL",
62487
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES,
62488
+ signal
62489
+ }, (error, stdout) => {
62490
+ if (signal?.aborted) {
62491
+ reject(signal.reason);
62492
+ return;
62493
+ }
62494
+ resolve(error ? /* @__PURE__ */ new Set() : parseIgnoredPaths(stdout));
62495
+ }).stdin?.end(buildInput(relativePaths));
62496
+ });
62497
+ };
62320
62498
  const lastSourceMappingUrl = (fileContent) => {
62321
62499
  const lastReference = [...fileContent.matchAll(/\/\/# sourceMappingURL=(\S+)/g)].at(-1);
62322
62500
  return lastReference ? lastReference[1] : null;
@@ -62421,6 +62599,9 @@ function* walkSourceTreeFiles(rootDirectory) {
62421
62599
  }
62422
62600
  }
62423
62601
  }
62602
+ const yieldToEventLoop = () => new Promise((resolve) => {
62603
+ setImmediate(resolve);
62604
+ });
62424
62605
  const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
62425
62606
  const entries = [];
62426
62607
  for (const relativePath of relativePaths) {
@@ -62434,23 +62615,55 @@ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
62434
62615
  }
62435
62616
  return entries;
62436
62617
  };
62437
- const gitLsFiles = (rootDirectory, flags) => {
62618
+ const collectSizedSourceFilesCooperative = async (rootDirectory, relativePaths, signal) => {
62619
+ const entries = [];
62620
+ let sliceStartedAt = performance.now();
62621
+ for (const relativePath of relativePaths) {
62622
+ signal?.throwIfAborted();
62623
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
62624
+ const sizeBytes = statSourceFileSize(absolutePath);
62625
+ if (!isLargeMinifiedFile(absolutePath, sizeBytes)) entries.push({
62626
+ path: relativePath,
62627
+ sizeBytes: sizeBytes ?? 0
62628
+ });
62629
+ if (performance.now() - sliceStartedAt >= 12) {
62630
+ await yieldToEventLoop();
62631
+ sliceStartedAt = performance.now();
62632
+ }
62633
+ }
62634
+ return entries;
62635
+ };
62636
+ const parseGitSourceFilePaths = (output) => {
62637
+ const trackedPaths = /* @__PURE__ */ new Set();
62638
+ const untrackedPaths = [];
62639
+ for (const entry of output.split("\0")) {
62640
+ if (entry.length === 0) continue;
62641
+ const trackedPath = /^[0-7]{6} [0-9a-f]+ [0-3]\t([\s\S]+)$/.exec(entry)?.[1];
62642
+ if (trackedPath === void 0) untrackedPaths.push(entry);
62643
+ else trackedPaths.add(trackedPath);
62644
+ }
62645
+ return {
62646
+ trackedPaths: [...trackedPaths],
62647
+ untrackedPaths
62648
+ };
62649
+ };
62650
+ const listGitSourceFilePaths = (rootDirectory) => {
62438
62651
  const result = spawnSync("git", [
62439
62652
  "ls-files",
62440
62653
  "-z",
62441
- ...flags
62654
+ "--stage",
62655
+ "--others",
62656
+ "--exclude-standard"
62442
62657
  ], {
62443
62658
  cwd: rootDirectory,
62444
62659
  encoding: "utf-8",
62445
62660
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
62446
62661
  });
62447
62662
  if (result.error || result.status !== 0) return null;
62448
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0);
62663
+ return parseGitSourceFilePaths(result.stdout);
62449
62664
  };
62450
- const listSourceFilesViaGit = (rootDirectory) => {
62451
- const trackedPaths = gitLsFiles(rootDirectory, ["--cached"]);
62452
- const untrackedPaths = gitLsFiles(rootDirectory, ["--others", "--exclude-standard"]);
62453
- if (trackedPaths === null || untrackedPaths === null) return null;
62665
+ const collectGitCandidateSourceFilePaths = (rootDirectory, paths) => {
62666
+ const { trackedPaths, untrackedPaths } = paths;
62454
62667
  const emitDuplicateJsPaths = collectTypeScriptEmitDuplicateJsPaths({
62455
62668
  trackedPaths: new Set(trackedPaths),
62456
62669
  untrackedPaths,
@@ -62458,12 +62671,65 @@ const listSourceFilesViaGit = (rootDirectory) => {
62458
62671
  });
62459
62672
  return [...trackedPaths, ...untrackedPaths].filter((filePath) => isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath) && !emitDuplicateJsPaths.has(filePath));
62460
62673
  };
62674
+ const filterGitSourceFilePaths = (rootDirectory, paths) => {
62675
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
62676
+ const linguistIgnoredPaths = collectGitLinguistIgnoredPaths(rootDirectory, candidatePaths);
62677
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
62678
+ };
62679
+ const listSourceFilesViaGit = (rootDirectory) => {
62680
+ const paths = listGitSourceFilePaths(rootDirectory);
62681
+ return paths === null ? null : filterGitSourceFilePaths(rootDirectory, paths);
62682
+ };
62683
+ const listSourceFilesViaGitCooperative = async (rootDirectory, signal) => {
62684
+ const paths = await new Promise((resolve, reject) => {
62685
+ signal?.throwIfAborted();
62686
+ execFile("git", [
62687
+ "ls-files",
62688
+ "-z",
62689
+ "--stage",
62690
+ "--others",
62691
+ "--exclude-standard"
62692
+ ], {
62693
+ cwd: rootDirectory,
62694
+ encoding: "utf-8",
62695
+ killSignal: "SIGKILL",
62696
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
62697
+ signal
62698
+ }, (error, stdout) => {
62699
+ if (signal?.aborted) {
62700
+ reject(signal.reason);
62701
+ return;
62702
+ }
62703
+ resolve(error ? null : parseGitSourceFilePaths(stdout));
62704
+ });
62705
+ });
62706
+ if (paths === null) return null;
62707
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
62708
+ const linguistIgnoredPaths = await collectGitLinguistIgnoredPathsCooperative(rootDirectory, candidatePaths, signal);
62709
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
62710
+ };
62461
62711
  const listSourceFilesViaFilesystem = (rootDirectory) => {
62462
62712
  const filePaths = [];
62463
62713
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
62464
62714
  return filePaths;
62465
62715
  };
62466
- const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
62716
+ const listSourceFilesViaFilesystemCooperative = async (rootDirectory, signal) => {
62717
+ const filePaths = [];
62718
+ let sliceStartedAt = performance.now();
62719
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
62720
+ signal?.throwIfAborted();
62721
+ if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
62722
+ if (performance.now() - sliceStartedAt >= 12) {
62723
+ await yieldToEventLoop();
62724
+ sliceStartedAt = performance.now();
62725
+ }
62726
+ }
62727
+ return filePaths;
62728
+ };
62729
+ const listSourceFilePaths = (rootDirectory) => (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort();
62730
+ const listSourceFilePathsCooperative = async (rootDirectory, signal) => (await listSourceFilesViaGitCooperative(rootDirectory, signal) ?? await listSourceFilesViaFilesystemCooperative(rootDirectory, signal)).sort();
62731
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilePaths(rootDirectory));
62732
+ const listSourceFilesCooperative = async (rootDirectory, signal) => (await collectSizedSourceFilesCooperative(rootDirectory, await listSourceFilePathsCooperative(rootDirectory, signal), signal)).map((entry) => entry.path);
62467
62733
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
62468
62734
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
62469
62735
  const UTF8_BOM_CHARACTER = "";
@@ -62589,7 +62855,12 @@ const mergeCompilerOptions = (inherited, current) => {
62589
62855
  };
62590
62856
  };
62591
62857
  const readResolvedCompilerOptions = (tsConfigPath, extendsDepth, visitedPaths) => {
62592
- const realPath = NFS.realpathSync.native(tsConfigPath);
62858
+ let realPath;
62859
+ try {
62860
+ realPath = NFS.realpathSync.native(tsConfigPath);
62861
+ } catch {
62862
+ return null;
62863
+ }
62593
62864
  if (visitedPaths.has(realPath)) return null;
62594
62865
  const tsConfig = readTsConfig(realPath);
62595
62866
  if (!tsConfig) return null;
@@ -63117,27 +63388,30 @@ const getSelectedObjectProperty = (expression, propertyName, analysis, visitedEx
63117
63388
  }
63118
63389
  return null;
63119
63390
  };
63120
- const configExpressionMayDefineProperty = (expression, propertyName, analysis) => {
63391
+ const configExpressionMayDefineProperty = (expression, propertyName, analysis, visitedExpressions = /* @__PURE__ */ new Set()) => {
63121
63392
  let resolvedExpression = expression;
63122
63393
  while (ts.isParenthesizedExpression(resolvedExpression) || ts.isAsExpression(resolvedExpression) || ts.isTypeAssertionExpression(resolvedExpression) || ts.isSatisfiesExpression(resolvedExpression) || ts.isNonNullExpression(resolvedExpression)) resolvedExpression = resolvedExpression.expression;
63394
+ if (visitedExpressions.has(resolvedExpression)) return true;
63395
+ const nextVisitedExpressions = new Set(visitedExpressions);
63396
+ nextVisitedExpressions.add(resolvedExpression);
63123
63397
  if (ts.isIdentifier(resolvedExpression)) {
63124
63398
  if (analysis.localBindings.has(resolvedExpression.text)) {
63125
63399
  const localReference = analysis.localBindings.get(resolvedExpression.text);
63126
- return localReference?.expression ? configExpressionMayDefineProperty(localReference.expression, propertyName, localReference.analysis) : true;
63400
+ return localReference?.expression ? configExpressionMayDefineProperty(localReference.expression, propertyName, localReference.analysis, nextVisitedExpressions) : true;
63127
63401
  }
63128
63402
  const scopedBinding = getScopedConfigBinding(resolvedExpression);
63129
- if (scopedBinding.wasFound) return scopedBinding.initializer ? configExpressionMayDefineProperty(scopedBinding.initializer, propertyName, analysis) : true;
63403
+ if (scopedBinding.wasFound) return scopedBinding.initializer ? configExpressionMayDefineProperty(scopedBinding.initializer, propertyName, analysis, nextVisitedExpressions) : true;
63130
63404
  const topLevelBinding = getTopLevelBinding(analysis.sourceFile, resolvedExpression.text);
63131
- return topLevelBinding && ts.isExpression(topLevelBinding) ? configExpressionMayDefineProperty(topLevelBinding, propertyName, analysis) : true;
63405
+ return topLevelBinding && ts.isExpression(topLevelBinding) ? configExpressionMayDefineProperty(topLevelBinding, propertyName, analysis, nextVisitedExpressions) : true;
63132
63406
  }
63133
63407
  if (ts.isConditionalExpression(resolvedExpression)) {
63134
- if (isStaticallyTruthyConfigExpression(resolvedExpression.condition, analysis)) return configExpressionMayDefineProperty(resolvedExpression.whenTrue, propertyName, analysis);
63135
- if (isStaticallyDisabledConfigExpression(resolvedExpression.condition, analysis)) return configExpressionMayDefineProperty(resolvedExpression.whenFalse, propertyName, analysis);
63136
- return configExpressionMayDefineProperty(resolvedExpression.whenTrue, propertyName, analysis) || configExpressionMayDefineProperty(resolvedExpression.whenFalse, propertyName, analysis);
63408
+ if (isStaticallyTruthyConfigExpression(resolvedExpression.condition, analysis)) return configExpressionMayDefineProperty(resolvedExpression.whenTrue, propertyName, analysis, nextVisitedExpressions);
63409
+ if (isStaticallyDisabledConfigExpression(resolvedExpression.condition, analysis)) return configExpressionMayDefineProperty(resolvedExpression.whenFalse, propertyName, analysis, nextVisitedExpressions);
63410
+ return configExpressionMayDefineProperty(resolvedExpression.whenTrue, propertyName, analysis, nextVisitedExpressions) || configExpressionMayDefineProperty(resolvedExpression.whenFalse, propertyName, analysis, nextVisitedExpressions);
63137
63411
  }
63138
63412
  if (!ts.isObjectLiteralExpression(resolvedExpression)) return true;
63139
63413
  return resolvedExpression.properties.some((property) => {
63140
- if (ts.isSpreadAssignment(property)) return configExpressionMayDefineProperty(property.expression, propertyName, analysis);
63414
+ if (ts.isSpreadAssignment(property)) return configExpressionMayDefineProperty(property.expression, propertyName, analysis, nextVisitedExpressions);
63141
63415
  if (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property) || ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property)) {
63142
63416
  const staticPropertyName = getStaticPropertyName$2(property.name);
63143
63417
  return staticPropertyName === null || staticPropertyName === propertyName;
@@ -63213,7 +63487,13 @@ const analyzeConfigModuleExport = (filePath, exportName, allowCompilerTransform,
63213
63487
  if (visitedModules.has(moduleVisitKey)) return false;
63214
63488
  const nextVisitedModules = new Set(visitedModules);
63215
63489
  nextVisitedModules.add(moduleVisitKey);
63216
- return analyzeConfigSourceFileExport(parseConfigSourceFile(filePath, NFS.readFileSync(filePath, "utf-8")), filePath, exportName, allowCompilerTransform, importDepth, nextVisitedModules, argumentsList, argumentAnalysis);
63490
+ let sourceText;
63491
+ try {
63492
+ sourceText = NFS.readFileSync(filePath, "utf-8");
63493
+ } catch {
63494
+ return false;
63495
+ }
63496
+ return analyzeConfigSourceFileExport(parseConfigSourceFile(filePath, sourceText), filePath, exportName, allowCompilerTransform, importDepth, nextVisitedModules, argumentsList, argumentAnalysis);
63217
63497
  };
63218
63498
  const analyzeConfigSourceFileExport = (sourceFile, filePath, exportName, allowCompilerTransform, importDepth, visitedModules, argumentsList = [], argumentAnalysis) => {
63219
63499
  const analysis = {
@@ -64041,13 +64321,7 @@ const hasReactDependency = (packageJson) => {
64041
64321
  };
64042
64322
  return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
64043
64323
  };
64044
- const getPreactVersion = (packageJson) => {
64045
- return {
64046
- ...packageJson.peerDependencies,
64047
- ...packageJson.dependencies,
64048
- ...packageJson.devDependencies
64049
- }.preact ?? null;
64050
- };
64324
+ const getPreactVersion = (packageJson) => getDependencySpec(packageJson, "preact");
64051
64325
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, sourceDirectory = rootDirectory, sourcePackageJson = rootPackageJson, version }) => {
64052
64326
  if (version === null || !isCatalogReference(version)) return version;
64053
64327
  const catalogName = extractCatalogName(version);
@@ -64792,7 +65066,16 @@ const getBrowserslistProtectionRequirement = (directory) => {
64792
65066
  if (!queries) return void 0;
64793
65067
  let requirement;
64794
65068
  let hasUnsupportedTarget = false;
64795
- for (const browserTarget of (0, import_browserslist.default)(queries)) {
65069
+ const previousIgnoreOldData = process.env.BROWSERSLIST_IGNORE_OLD_DATA;
65070
+ let browserTargets = [];
65071
+ try {
65072
+ process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true";
65073
+ browserTargets = (0, import_browserslist.default)(queries);
65074
+ } finally {
65075
+ if (previousIgnoreOldData === void 0) delete process.env.BROWSERSLIST_IGNORE_OLD_DATA;
65076
+ else process.env.BROWSERSLIST_IGNORE_OLD_DATA = previousIgnoreOldData;
65077
+ }
65078
+ for (const browserTarget of browserTargets) {
64796
65079
  const browserRequirement = getBrowserProtectionRequirement(browserTarget);
64797
65080
  if (browserRequirement === "unsupported") hasUnsupportedTarget = true;
64798
65081
  if (browserRequirement === "noreferrer") requirement = "noreferrer";
@@ -64902,9 +65185,9 @@ const clearProjectCache = () => {
64902
65185
  * framework-agnostic rules. Throws only when the directory has nothing
64903
65186
  * to scan (no enclosing project and no source files of its own).
64904
65187
  */
64905
- const discoverProjectWithoutPackageJson = (directory) => {
64906
- const sourceFileCount = countSourceFiles(directory);
64907
- const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
65188
+ const discoverProjectWithoutPackageJson = (directory, options) => {
65189
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
65190
+ const hasOwnTsConfig = isFile(Path.join(directory, "tsconfig.json"));
64908
65191
  const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
64909
65192
  const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
64910
65193
  if (enclosingProject !== null) return {
@@ -64970,12 +65253,15 @@ const discoverProjectWithoutPackageJson = (directory) => {
64970
65253
  sourceFileCount
64971
65254
  };
64972
65255
  };
64973
- const discoverProject = (directory) => {
65256
+ const discoverProject = (directory, options = {}) => {
64974
65257
  const cached = cachedProjectInfos.get(directory);
64975
- if (cached !== void 0) return cached;
65258
+ if (cached !== void 0) return options.sourceFileCount === void 0 ? cached : {
65259
+ ...cached,
65260
+ sourceFileCount: options.sourceFileCount
65261
+ };
64976
65262
  const packageJsonPath = Path.join(directory, "package.json");
64977
65263
  if (!isFile(packageJsonPath)) {
64978
- const synthesized = discoverProjectWithoutPackageJson(directory);
65264
+ const synthesized = discoverProjectWithoutPackageJson(directory, options);
64979
65265
  cachedProjectInfos.set(directory, synthesized);
64980
65266
  return synthesized;
64981
65267
  }
@@ -65037,8 +65323,8 @@ const discoverProject = (directory) => {
65037
65323
  const tailwindVersion = tailwindcss.version;
65038
65324
  const zodVersion = zod.version;
65039
65325
  const projectName = packageJson.name ?? Path.basename(directory);
65040
- const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
65041
- const sourceFileCount = countSourceFiles(directory);
65326
+ const hasTypeScript = isFile(Path.join(directory, "tsconfig.json"));
65327
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
65042
65328
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
65043
65329
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
65044
65330
  rootDirectory: directory,
@@ -65167,134 +65453,6 @@ const discoverProject = (directory) => {
65167
65453
  * single source file has nothing to analyze.
65168
65454
  */
65169
65455
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
65170
- const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
65171
- var InvalidGlobPatternError = class extends Error {
65172
- pattern;
65173
- reason;
65174
- constructor(pattern, reason) {
65175
- super(`Invalid glob pattern ${JSON.stringify(pattern)}: ${reason}`);
65176
- this.name = "InvalidGlobPatternError";
65177
- this.pattern = pattern;
65178
- this.reason = reason;
65179
- }
65180
- };
65181
- const assertGlobPattern = (condition, pattern, reason) => {
65182
- if (!condition) throw new InvalidGlobPatternError(pattern, reason);
65183
- };
65184
- const countGlobWildcards = (pattern) => (pattern.match(/[*?]/g) ?? []).length;
65185
- const normalizeGlobPattern = (pattern) => pattern.replace(/\\/g, "/").replace(/^\//, "");
65186
- const PICOMATCH_OPTIONS = {
65187
- dot: true,
65188
- strictSlashes: false,
65189
- windows: false
65190
- };
65191
- const compileGlobPattern = (rawPattern) => {
65192
- assertGlobPattern(typeof rawPattern === "string" && rawPattern.length > 0, String(rawPattern), "pattern must be a non-empty string.");
65193
- assertGlobPattern(rawPattern.length <= MAX_GLOB_PATTERN_LENGTH_CHARS, rawPattern, `pattern length ${rawPattern.length} exceeds the maximum of ${MAX_GLOB_PATTERN_LENGTH_CHARS} characters.`);
65194
- const wildcardCount = countGlobWildcards(rawPattern);
65195
- assertGlobPattern(wildcardCount <= 24, rawPattern, `pattern uses ${wildcardCount} wildcards (\`*\` / \`?\`), exceeding the maximum of 24. This guards against catastrophic backtracking from pathological patterns; split the pattern into multiple smaller entries.`);
65196
- try {
65197
- return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
65198
- } catch (caughtError) {
65199
- throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
65200
- }
65201
- };
65202
- const compileGlobPatternsLenient = (patterns, onInvalid) => {
65203
- const compiled = [];
65204
- for (const pattern of patterns) try {
65205
- compiled.push(compileGlobPattern(pattern));
65206
- } catch (caughtError) {
65207
- if (!(caughtError instanceof InvalidGlobPatternError)) throw caughtError;
65208
- onInvalid(caughtError);
65209
- }
65210
- return compiled;
65211
- };
65212
- const toRelativePath = (filePath, rootDirectory) => {
65213
- const normalizedFilePath = filePath.replace(/\\/g, "/");
65214
- const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
65215
- if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
65216
- return normalizedFilePath.replace(/^\.\//, "");
65217
- };
65218
- const warnConfigIssue = (message) => {
65219
- process.stderr.write(`[react-doctor] ${message}\n`);
65220
- };
65221
- const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
65222
- const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65223
- const validateOverrideEntry = (entry, index) => {
65224
- if (!isPlainObject(entry)) {
65225
- warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
65226
- return null;
65227
- }
65228
- if (!isStringArray(entry.files)) {
65229
- warnConfigIssue(`ignore.overrides[${index}].files must be an array of strings; ignoring this entry.`);
65230
- return null;
65231
- }
65232
- if (entry.rules !== void 0 && !isStringArray(entry.rules)) {
65233
- warnConfigIssue(`ignore.overrides[${index}].rules must be an array of "plugin/rule" strings or omitted; treating as missing (override would suppress every rule for the matched files).`);
65234
- return { files: entry.files };
65235
- }
65236
- return entry.rules === void 0 ? { files: entry.files } : {
65237
- files: entry.files,
65238
- rules: entry.rules
65239
- };
65240
- };
65241
- const compileIgnoreOverrides = (userConfig) => {
65242
- const overrides = userConfig?.ignore?.overrides;
65243
- if (overrides === void 0) return [];
65244
- if (!Array.isArray(overrides)) {
65245
- warnConfigIssue(`ignore.overrides must be an array of { files, rules } entries; ignoring.`);
65246
- return [];
65247
- }
65248
- return overrides.flatMap((entry, index) => {
65249
- const validated = validateOverrideEntry(entry, index);
65250
- if (!validated) return [];
65251
- const filePatterns = compileGlobPatternsLenient(collectStringList(validated.files), (error) => warnConfigIssue(`ignore.overrides[${index}]: ${error.message}`));
65252
- if (filePatterns.length === 0) return [];
65253
- return [{
65254
- filePatterns,
65255
- ruleIds: new Set(collectStringList(validated.rules))
65256
- }];
65257
- });
65258
- };
65259
- const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) => {
65260
- if (overrides.length === 0) return false;
65261
- const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
65262
- const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
65263
- return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && (override.ruleIds.size === 0 || override.ruleIds.has(ruleIdentifier)));
65264
- };
65265
- const SEVERITY_FOR_OVERRIDE = {
65266
- error: "error",
65267
- warn: "warning"
65268
- };
65269
- const restampSeverity = (diagnostic, override) => {
65270
- const targetSeverity = SEVERITY_FOR_OVERRIDE[override];
65271
- if (diagnostic.severity === targetSeverity) return diagnostic;
65272
- return {
65273
- ...diagnostic,
65274
- severity: targetSeverity
65275
- };
65276
- };
65277
- /**
65278
- * Assembles the internal `RuleSeverityControls` shape from a user
65279
- * config's top-level `rules` and `categories` fields — the
65280
- * ESLint / oxlint-shaped severity surface.
65281
- *
65282
- * Returns `undefined` when neither field is present so the common
65283
- * path (no severity config at all) stays allocation-free for
65284
- * downstream consumers.
65285
- */
65286
- const buildRuleSeverityControls = (config) => {
65287
- if (!config) return void 0;
65288
- if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
65289
- return {
65290
- ...config.rules !== void 0 ? { rules: config.rules } : {},
65291
- ...config.categories !== void 0 ? { categories: config.categories } : {},
65292
- ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
65293
- };
65294
- };
65295
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
65296
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
65297
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
65298
65456
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
65299
65457
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
65300
65458
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -65433,6 +65591,139 @@ const getEquivalentRuleKeys = (ruleKey) => {
65433
65591
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
65434
65592
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
65435
65593
  };
65594
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
65595
+ var InvalidGlobPatternError = class extends Error {
65596
+ pattern;
65597
+ reason;
65598
+ constructor(pattern, reason) {
65599
+ super(`Invalid glob pattern ${JSON.stringify(pattern)}: ${reason}`);
65600
+ this.name = "InvalidGlobPatternError";
65601
+ this.pattern = pattern;
65602
+ this.reason = reason;
65603
+ }
65604
+ };
65605
+ const assertGlobPattern = (condition, pattern, reason) => {
65606
+ if (!condition) throw new InvalidGlobPatternError(pattern, reason);
65607
+ };
65608
+ const countGlobWildcards = (pattern) => (pattern.match(/[*?]/g) ?? []).length;
65609
+ const normalizeGlobPattern = (pattern) => pattern.replace(/\\/g, "/").replace(/^\//, "");
65610
+ const PICOMATCH_OPTIONS = {
65611
+ dot: true,
65612
+ strictSlashes: false,
65613
+ windows: false
65614
+ };
65615
+ const compileGlobPattern = (rawPattern) => {
65616
+ assertGlobPattern(typeof rawPattern === "string" && rawPattern.length > 0, String(rawPattern), "pattern must be a non-empty string.");
65617
+ assertGlobPattern(rawPattern.length <= MAX_GLOB_PATTERN_LENGTH_CHARS, rawPattern, `pattern length ${rawPattern.length} exceeds the maximum of ${MAX_GLOB_PATTERN_LENGTH_CHARS} characters.`);
65618
+ const wildcardCount = countGlobWildcards(rawPattern);
65619
+ assertGlobPattern(wildcardCount <= 24, rawPattern, `pattern uses ${wildcardCount} wildcards (\`*\` / \`?\`), exceeding the maximum of 24. This guards against catastrophic backtracking from pathological patterns; split the pattern into multiple smaller entries.`);
65620
+ try {
65621
+ return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
65622
+ } catch (caughtError) {
65623
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
65624
+ }
65625
+ };
65626
+ const compileGlobPatternsLenient = (patterns, onInvalid) => {
65627
+ const compiled = [];
65628
+ for (const pattern of patterns) try {
65629
+ compiled.push(compileGlobPattern(pattern));
65630
+ } catch (caughtError) {
65631
+ if (!(caughtError instanceof InvalidGlobPatternError)) throw caughtError;
65632
+ onInvalid(caughtError);
65633
+ }
65634
+ return compiled;
65635
+ };
65636
+ const toRelativePath = (filePath, rootDirectory) => {
65637
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
65638
+ const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
65639
+ if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
65640
+ return normalizedFilePath.replace(/^\.\//, "");
65641
+ };
65642
+ const warnConfigIssue = (message) => {
65643
+ process.stderr.write(`[react-doctor] ${message}\n`);
65644
+ };
65645
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
65646
+ const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65647
+ const hasMatchingRuleOverride = (ruleIds, ruleIdentifier) => {
65648
+ if (ruleIds.size === 0) return true;
65649
+ for (const ruleId of ruleIds) if (isSameRuleKey(ruleId, ruleIdentifier)) return true;
65650
+ return false;
65651
+ };
65652
+ const validateOverrideEntry = (entry, index) => {
65653
+ if (!isPlainObject(entry)) {
65654
+ warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
65655
+ return null;
65656
+ }
65657
+ if (!isStringArray(entry.files)) {
65658
+ warnConfigIssue(`ignore.overrides[${index}].files must be an array of strings; ignoring this entry.`);
65659
+ return null;
65660
+ }
65661
+ if (entry.rules !== void 0 && !isStringArray(entry.rules)) {
65662
+ warnConfigIssue(`ignore.overrides[${index}].rules must be an array of "plugin/rule" strings or omitted; treating as missing (override would suppress every rule for the matched files).`);
65663
+ return { files: entry.files };
65664
+ }
65665
+ return entry.rules === void 0 ? { files: entry.files } : {
65666
+ files: entry.files,
65667
+ rules: entry.rules
65668
+ };
65669
+ };
65670
+ const compileIgnoreOverrides = (userConfig) => {
65671
+ const overrides = userConfig?.ignore?.overrides;
65672
+ if (overrides === void 0) return [];
65673
+ if (!Array.isArray(overrides)) {
65674
+ warnConfigIssue(`ignore.overrides must be an array of { files, rules } entries; ignoring.`);
65675
+ return [];
65676
+ }
65677
+ return overrides.flatMap((entry, index) => {
65678
+ const validated = validateOverrideEntry(entry, index);
65679
+ if (!validated) return [];
65680
+ const filePatterns = compileGlobPatternsLenient(collectStringList(validated.files), (error) => warnConfigIssue(`ignore.overrides[${index}]: ${error.message}`));
65681
+ if (filePatterns.length === 0) return [];
65682
+ return [{
65683
+ filePatterns,
65684
+ ruleIds: new Set(collectStringList(validated.rules))
65685
+ }];
65686
+ });
65687
+ };
65688
+ const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) => {
65689
+ if (overrides.length === 0) return false;
65690
+ const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
65691
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
65692
+ return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && hasMatchingRuleOverride(override.ruleIds, ruleIdentifier));
65693
+ };
65694
+ const SEVERITY_FOR_OVERRIDE = {
65695
+ error: "error",
65696
+ warn: "warning"
65697
+ };
65698
+ const restampSeverity = (diagnostic, override) => {
65699
+ const targetSeverity = SEVERITY_FOR_OVERRIDE[override];
65700
+ if (diagnostic.severity === targetSeverity) return diagnostic;
65701
+ return {
65702
+ ...diagnostic,
65703
+ severity: targetSeverity
65704
+ };
65705
+ };
65706
+ /**
65707
+ * Assembles the internal `RuleSeverityControls` shape from a user
65708
+ * config's top-level `rules` and `categories` fields — the
65709
+ * ESLint / oxlint-shaped severity surface.
65710
+ *
65711
+ * Returns `undefined` when neither field is present so the common
65712
+ * path (no severity config at all) stays allocation-free for
65713
+ * downstream consumers.
65714
+ */
65715
+ const buildRuleSeverityControls = (config) => {
65716
+ if (!config) return void 0;
65717
+ if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
65718
+ return {
65719
+ ...config.rules !== void 0 ? { rules: config.rules } : {},
65720
+ ...config.categories !== void 0 ? { categories: config.categories } : {},
65721
+ ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
65722
+ };
65723
+ };
65724
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
65725
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
65726
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
65436
65727
  const stripDescriptionTail = (ruleList) => {
65437
65728
  const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
65438
65729
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
@@ -65845,6 +66136,10 @@ const classifyByDirectoryCohort = (packageDirectory) => {
65845
66136
  current = parent;
65846
66137
  }
65847
66138
  };
66139
+ const clearPackageRoleCache = () => {
66140
+ cachedRoleByPackageDirectory.clear();
66141
+ cachedPackageDirectoryByFilename.clear();
66142
+ };
65848
66143
  const classifyPackageRole = (filename) => {
65849
66144
  if (!filename) return "unknown";
65850
66145
  const packageDirectory = findNearestPackageDirectory(filename);
@@ -66079,12 +66374,8 @@ const buildDiagnosticPipeline = (input) => {
66079
66374
  if (rule.tags.includes("migration-hint")) return false;
66080
66375
  return getFileContext(diagnostic.filePath) !== "production";
66081
66376
  };
66082
- const isRuleIgnored = (ruleIdentifier) => {
66083
- for (const ignored of ignoredRules) if (isSameRuleKey(ignored, ruleIdentifier)) return true;
66084
- return false;
66085
- };
66086
- const isAppOnlyRule = (ruleIdentifier) => {
66087
- for (const appOnlyRuleKey of APP_ONLY_RULE_KEYS) if (isSameRuleKey(appOnlyRuleKey, ruleIdentifier)) return true;
66377
+ const matchesRuleKey = (ruleIdentifier, candidates) => {
66378
+ for (const candidate of candidates) if (isSameRuleKey(candidate, ruleIdentifier)) return true;
66088
66379
  return false;
66089
66380
  };
66090
66381
  const isRnRawTextSuppressedByConfig = (diagnostic) => {
@@ -66126,12 +66417,13 @@ const buildDiagnosticPipeline = (input) => {
66126
66417
  if (explicitSeverityOverride === "off") return suppress(current, "config");
66127
66418
  if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
66128
66419
  }
66420
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
66129
66421
  if (explicitRuleOverride === void 0) {
66130
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
66422
+ if (matchesRuleKey(ruleIdentifier, APP_ONLY_RULE_KEYS) && isLibraryFile(current.filePath)) return null;
66131
66423
  }
66132
66424
  if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
66133
66425
  if (userConfig) {
66134
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
66426
+ if (matchesRuleKey(ruleIdentifier, ignoredRules)) return suppress(current, "config");
66135
66427
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
66136
66428
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
66137
66429
  if (isRnRawTextSuppressedByConfig(current)) return null;
@@ -66140,7 +66432,6 @@ const buildDiagnosticPipeline = (input) => {
66140
66432
  if (respectInlineDisables && current.line > 0) {
66141
66433
  const lines = getFileLines(current.filePath);
66142
66434
  if (lines) {
66143
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
66144
66435
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
66145
66436
  if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
66146
66437
  if (evaluation.nearMissHint) current = {
@@ -66394,7 +66685,8 @@ const readSystemFacts$1 = () => ({
66394
66685
  /**
66395
66686
  * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
66396
66687
  * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
66397
- * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
66688
+ * memory, and the measured automatic parallel-efficiency ceiling, then clamped
66689
+ * to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
66398
66690
  *
66399
66691
  * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
66400
66692
  * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
@@ -66410,7 +66702,7 @@ const readSystemFacts$1 = () => ({
66410
66702
  const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
66411
66703
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
66412
66704
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
66413
- return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
66705
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers, 10));
66414
66706
  };
66415
66707
  const resolveLintBatchOrdering = () => {
66416
66708
  return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
@@ -66423,24 +66715,22 @@ const resolveLintBatchOrdering = () => {
66423
66715
  */
66424
66716
  var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
66425
66717
  /**
66426
- * Effect-side cap on the lint phase. The env var lets CI / eval runners
66427
- * raise the phase budget for slow large repos without recompiling.
66718
+ * Optional Effect-side cap on the lint phase. Unbounded unless explicitly
66719
+ * configured through the env var or a test layer.
66428
66720
  * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
66429
66721
  */
66430
- var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
66722
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", null) }) {};
66431
66723
  /**
66432
- * Effect-side cap on the dead-code phase, sitting above the in-worker
66433
- * timeout as a runtime-independent backstop. The env var raises it for
66434
- * type-heavy projects; tests override via
66724
+ * Optional Effect-side cap on the dead-code phase. Unbounded unless explicitly
66725
+ * configured through the env var or a test layer. Tests override via
66435
66726
  * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
66436
66727
  */
66437
- var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
66728
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", null) }) {};
66438
66729
  /**
66439
- * Overall scan deadline backstop, bounding everything the per-phase
66440
- * timeouts don't (wedged git / IO). The env var raises it for very
66441
- * large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
66730
+ * Optional overall scan deadline. Unbounded unless explicitly configured
66731
+ * through the env var or a test layer.
66442
66732
  */
66443
- var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
66733
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", null) }) {};
66444
66734
  /**
66445
66735
  * Wall-clock budget for the supply-chain check when it runs on a background
66446
66736
  * fiber overlapping the lint pass. Reads from the env var on startup so the
@@ -66513,14 +66803,14 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
66513
66803
  /**
66514
66804
  * How the full-scan lint pass plans its file batches. `"cost"` (the default)
66515
66805
  * builds size-balanced LPT batches (`planLintBatches`): the same mandatory
66516
- * batch count as greedy chunking (`ceil(files / 100)`), but every batch gets
66517
- * an even share of files AND bytes, so no 100-file chunk is a straggler while
66806
+ * batch count as greedy chunking, but every batch gets an even share of files
66807
+ * AND bytes, so no full chunk is a straggler while
66518
66808
  * the remainder-batch worker idles — and the heavy files are SPREAD across
66519
66809
  * batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
66520
66810
  * lacked (it packed the heaviest files into one wave-1 straggler batch,
66521
66811
  * measurably regressing size-skewed repos, which is why it never earned the
66522
66812
  * default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
66523
- * rollback hatch to plain greedy 100-file chunking in discovery order. Tests
66813
+ * rollback hatch to plain greedy fixed-size chunking in discovery order. Tests
66524
66814
  * override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
66525
66815
  * never reach this — they pass user-scoped `includePaths` that skip discovery
66526
66816
  * and stay in arrival order; only the full-scan branch reads it.
@@ -66615,6 +66905,7 @@ const DEFAULT_SURFACE_EXCLUDED_TAGS = {
66615
66905
  score: ["design"],
66616
66906
  ciFailure: ["design"]
66617
66907
  };
66908
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66618
66909
  const VALID_RULE_SEVERITIES = [
66619
66910
  "error",
66620
66911
  "warn",
@@ -66657,7 +66948,6 @@ const SURFACE_CONTROL_FIELD_NAMES = [
66657
66948
  ];
66658
66949
  const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
66659
66950
  const SEVERITY_FIELD_NAMES = ["rules", "categories"];
66660
- const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66661
66951
  const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
66662
66952
  const isRuleSeverity = (value) => typeof value === "string" && VALID_RULE_SEVERITIES.includes(value);
66663
66953
  const coerceMaybeBooleanString = (fieldName, value) => {
@@ -66684,8 +66974,45 @@ const validateStringArrayField = (fieldName, value) => {
66684
66974
  return false;
66685
66975
  });
66686
66976
  };
66977
+ const validateIgnoreOverride = (rawOverride, index) => {
66978
+ const fieldName = `ignore.overrides[${index}]`;
66979
+ if (!isRecord(rawOverride)) {
66980
+ warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawOverride}); ignoring this override.`);
66981
+ return;
66982
+ }
66983
+ const files = validateStringArrayField(`${fieldName}.files`, rawOverride.files);
66984
+ if (files === void 0) return void 0;
66985
+ const validatedOverride = { files };
66986
+ if (rawOverride.rules !== void 0) {
66987
+ const rules = validateStringArrayField(`${fieldName}.rules`, rawOverride.rules);
66988
+ if (rules !== void 0) validatedOverride.rules = rules;
66989
+ }
66990
+ return validatedOverride;
66991
+ };
66992
+ const validateIgnoreField = (rawIgnore) => {
66993
+ if (!isRecord(rawIgnore)) {
66994
+ warnConfigIssue(`config field "ignore" must be an object (got ${typeof rawIgnore}); ignoring this field.`);
66995
+ return;
66996
+ }
66997
+ const validatedIgnore = {};
66998
+ for (const fieldName of [
66999
+ "rules",
67000
+ "files",
67001
+ "tags"
67002
+ ]) {
67003
+ if (rawIgnore[fieldName] === void 0) continue;
67004
+ const validatedValues = validateStringArrayField(`ignore.${fieldName}`, rawIgnore[fieldName]);
67005
+ if (validatedValues !== void 0) validatedIgnore[fieldName] = validatedValues;
67006
+ }
67007
+ if (rawIgnore.overrides !== void 0) if (!Array.isArray(rawIgnore.overrides)) warnConfigIssue(`config field "ignore.overrides" must be an array (got ${typeof rawIgnore.overrides}); ignoring this field.`);
67008
+ else validatedIgnore.overrides = rawIgnore.overrides.flatMap((rawOverride, index) => {
67009
+ const validatedOverride = validateIgnoreOverride(rawOverride, index);
67010
+ return validatedOverride ? [validatedOverride] : [];
67011
+ });
67012
+ return validatedIgnore;
67013
+ };
66687
67014
  const validateSurfaceControls = (surface, rawControls) => {
66688
- if (!isPlainObject$1(rawControls)) {
67015
+ if (!isRecord(rawControls)) {
66689
67016
  warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
66690
67017
  return;
66691
67018
  }
@@ -66709,7 +67036,7 @@ const validateSurfaceControls = (surface, rawControls) => {
66709
67036
  return validatedSurfaceControls;
66710
67037
  };
66711
67038
  const validateSurfacesField = (rawSurfaces) => {
66712
- if (!isPlainObject$1(rawSurfaces)) {
67039
+ if (!isRecord(rawSurfaces)) {
66713
67040
  warnConfigIssue(`config field "surfaces" must be an object (got ${typeof rawSurfaces}); ignoring this field.`);
66714
67041
  return;
66715
67042
  }
@@ -66725,7 +67052,7 @@ const validateSurfacesField = (rawSurfaces) => {
66725
67052
  return validated;
66726
67053
  };
66727
67054
  const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
66728
- if (!isPlainObject$1(rawMap)) {
67055
+ if (!isRecord(rawMap)) {
66729
67056
  warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
66730
67057
  return;
66731
67058
  }
@@ -66759,6 +67086,7 @@ const validateConfigTypes = (config) => {
66759
67086
  for (const fieldName of BOOLEAN_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => coerceMaybeBooleanString(fieldName, value));
66760
67087
  for (const fieldName of STRING_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateString(fieldName, value));
66761
67088
  for (const fieldName of STRING_ARRAY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateStringArrayField(fieldName, value));
67089
+ applyFieldValidator(config, validated, "ignore", validateIgnoreField);
66762
67090
  applyFieldValidator(config, validated, "surfaces", validateSurfacesField);
66763
67091
  for (const fieldName of SEVERITY_FIELD_NAMES) applyFieldValidator(config, validated, fieldName, (value) => validateSeverityMap(fieldName, value, fieldName === "categories"));
66764
67092
  applyFieldValidator(config, validated, "plugins", (value) => validateStringArrayField("plugins", value));
@@ -67010,6 +67338,10 @@ const dedupeRelatedDiagnostics = (diagnostics) => {
67010
67338
  }
67011
67339
  return uniqueDiagnostics;
67012
67340
  };
67341
+ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
67342
+ const relativePath = Path.relative(parentAbsolutePath, childAbsolutePath);
67343
+ return Boolean(relativePath) && !relativePath.startsWith("..") && !Path.isAbsolute(relativePath);
67344
+ };
67013
67345
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
67014
67346
  const sortDiagnosticsStable = (diagnostics) => [...diagnostics].sort((left, right) => compareStrings(left.filePath, right.filePath) || left.line - right.line || left.column - right.column || compareStrings(left.plugin, right.plugin) || compareStrings(left.rule, right.rule) || compareStrings(left.severity, right.severity) || compareStrings(left.message, right.message));
67015
67347
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -68371,7 +68703,7 @@ const collectScriptMotionEvidence = (sourceFile, typeChecker, program) => {
68371
68703
  if (calleeEvidence.isReducedMotionHook && isExpressionValueConsumed(node)) evidence.hasReducedMotionHandling = true;
68372
68704
  }
68373
68705
  if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
68374
- const tagEvidence = ts.isJsxNamespacedName(node.tagName) ? EMPTY_MOTION_EXPRESSION_EVIDENCE : resolveMotionExpressionEvidence(node.tagName, typeChecker, program);
68706
+ const tagEvidence = ts.isIdentifier(node.tagName) || ts.isPropertyAccessExpression(node.tagName) ? resolveMotionExpressionEvidence(node.tagName, typeChecker, program) : EMPTY_MOTION_EXPRESSION_EVIDENCE;
68375
68707
  if (tagEvidence.isMotionComponent || tagEvidence.isMotionComponentFactory) evidence.hasMotionUse = true;
68376
68708
  if (tagEvidence.isMotionConfig && jsxElementHasReducedMotionConfiguration(node.attributes, typeChecker)) evidence.hasReducedMotionHandling = true;
68377
68709
  }
@@ -68586,7 +68918,7 @@ const readScannedFile = (candidate) => {
68586
68918
  return null;
68587
68919
  }
68588
68920
  };
68589
- function* collectSecurityScanFiles(rootDirectory) {
68921
+ function* collectSecurityScanFiles(rootDirectory, excludedDirectories = /* @__PURE__ */ new Set()) {
68590
68922
  const priorityCandidates = [];
68591
68923
  const artifactCandidates = [];
68592
68924
  const otherCandidates = [];
@@ -68601,7 +68933,7 @@ function* collectSecurityScanFiles(rootDirectory) {
68601
68933
  for (const entry of readDirectoryEntries(current.absolutePath)) {
68602
68934
  const absolutePath = Path.join(current.absolutePath, entry.name);
68603
68935
  if (entry.isDirectory()) {
68604
- if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) stack.push({
68936
+ if (!SKIPPED_DIRECTORY_NAMES.has(entry.name) && !excludedDirectories.has(Path.resolve(absolutePath))) stack.push({
68605
68937
  absolutePath,
68606
68938
  depth: current.depth + 1
68607
68939
  });
@@ -68782,10 +69114,8 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhe
68782
69114
  }
68783
69115
  return true;
68784
69116
  };
69117
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
68785
69118
  const shouldEnableRuleByDefaultStatus = (options) => options.defaultEnabled !== false || options.hasExplicitOverride === true || options.includeTagDefaults && options.hasIncludedTags;
68786
- const yieldToEventLoop = () => new Promise((resolve) => {
68787
- setImmediate(resolve);
68788
- });
68789
69119
  const createSecurityScanSession = (rootDirectory, options) => {
68790
69120
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
68791
69121
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
@@ -68838,19 +69168,35 @@ const createSecurityScanSession = (rootDirectory, options) => {
68838
69168
  };
68839
69169
  };
68840
69170
  const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
69171
+ const throwIfScanAborted = () => {
69172
+ options.signal?.throwIfAborted();
69173
+ };
69174
+ const didReachDeadline = () => {
69175
+ const hasDeadlineElapsed = options.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(options.deadlineEpochMs) === 0;
69176
+ if (hasDeadlineElapsed) options.onDeadlineExceeded?.();
69177
+ return hasDeadlineElapsed;
69178
+ };
69179
+ throwIfScanAborted();
68841
69180
  const session = createSecurityScanSession(rootDirectory, options);
68842
69181
  if (session === null) return [];
69182
+ if (didReachDeadline()) return [];
68843
69183
  let sliceStartedAt = performance.now();
68844
- for (const file of collectSecurityScanFiles(rootDirectory)) {
69184
+ for (const file of collectSecurityScanFiles(rootDirectory, options.excludedDirectories)) {
69185
+ throwIfScanAborted();
69186
+ if (didReachDeadline()) return session.diagnostics;
68845
69187
  if (file === null) {
68846
69188
  if (performance.now() - sliceStartedAt >= 12) {
68847
69189
  await yieldToEventLoop();
69190
+ throwIfScanAborted();
69191
+ if (didReachDeadline()) return session.diagnostics;
68848
69192
  sliceStartedAt = performance.now();
68849
69193
  }
68850
69194
  continue;
68851
69195
  }
68852
69196
  for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
68853
69197
  await yieldToEventLoop();
69198
+ throwIfScanAborted();
69199
+ if (didReachDeadline()) return session.diagnostics;
68854
69200
  sliceStartedAt = performance.now();
68855
69201
  }
68856
69202
  }
@@ -69013,25 +69359,18 @@ const collectIgnorePatterns = (rootDirectory) => {
69013
69359
  cachedPatternsByRoot.set(rootDirectory, patterns);
69014
69360
  return patterns;
69015
69361
  };
69016
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
69017
- const KNIP_JSON_FILENAME = "knip.json";
69018
- const readJsonFileSafe = (filePath) => {
69019
- let rawContents;
69020
- try {
69021
- rawContents = fs.readFileSync(filePath, "utf-8");
69022
- } catch {
69023
- return null;
69024
- }
69362
+ const failOpenReadJson = (filePath, fallback) => {
69025
69363
  try {
69026
- return JSON.parse(rawContents);
69364
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
69027
69365
  } catch {
69028
- return null;
69366
+ return fallback;
69029
69367
  }
69030
69368
  };
69369
+ const KNIP_JSON_FILENAME = "knip.json";
69031
69370
  const readKnipConfig = (rootDirectory) => {
69032
- const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
69371
+ const knipJson = failOpenReadJson(path.join(rootDirectory, KNIP_JSON_FILENAME), null);
69033
69372
  if (isRecord(knipJson)) return knipJson;
69034
- const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
69373
+ const packageJson = failOpenReadJson(path.join(rootDirectory, "package.json"), null);
69035
69374
  const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
69036
69375
  return isRecord(packageKnipConfig) ? packageKnipConfig : null;
69037
69376
  };
@@ -69079,13 +69418,6 @@ const atomicWriteJson = (filePath, value) => {
69079
69418
  return;
69080
69419
  }
69081
69420
  };
69082
- const failOpenReadJson = (filePath, fallback) => {
69083
- try {
69084
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
69085
- } catch {
69086
- return fallback;
69087
- }
69088
- };
69089
69421
  const hashFileContents = (filePath) => {
69090
69422
  try {
69091
69423
  return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
@@ -69764,7 +70096,6 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
69764
70096
  return true;
69765
70097
  };
69766
70098
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
69767
- const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
69768
70099
  /**
69769
70100
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
69770
70101
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -69772,25 +70103,28 @@ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs
69772
70103
  * tipped over by any concurrent load — silently dropping every dead-code
69773
70104
  * finding. Scaling the budget with file count (and inversely with the core
69774
70105
  * share when overlapped) lets the pass complete, while the ceiling still
69775
- * reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
69776
- * and the Effect-side phase backstop that sits a margin above it.
70106
+ * reclaims a genuinely wedged worker.
69777
70107
  */
69778
70108
  const resolveDeadCodeTimeout = (input) => {
69779
70109
  const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
69780
- const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
69781
- return {
69782
- workerTimeoutMs,
69783
- phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
69784
- };
70110
+ return { workerTimeoutMs: Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor))) };
69785
70111
  };
69786
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
70112
+ const resolveLintIncludePaths = (rootDirectory, userConfig, sourceFiles) => {
69787
70113
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
69788
70114
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
69789
- return listSourceFiles(rootDirectory).filter((filePath) => {
70115
+ return (sourceFiles ?? listSourceFiles(rootDirectory)).filter((filePath) => {
69790
70116
  if (!isLintableSourceFile(filePath)) return false;
69791
70117
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
69792
70118
  });
69793
70119
  };
70120
+ const filterPathsOutsideDirectories = (input) => {
70121
+ const excludedPrefixes = input.excludedDirectories.map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, input.rootDirectory)).map((excludedDirectory) => Path.relative(input.rootDirectory, excludedDirectory).replaceAll("\\", "/")).sort((left, right) => left.length - right.length).filter((candidatePrefix, index, prefixes) => !prefixes.slice(0, index).some((prefix) => candidatePrefix.startsWith(`${prefix}/`)));
70122
+ if (excludedPrefixes.length === 0) return [...input.relativePaths];
70123
+ return input.relativePaths.filter((relativePath) => {
70124
+ const normalizedRelativePath = relativePath.replaceAll("\\", "/").replace(/^\.\//, "");
70125
+ return !excludedPrefixes.some((excludedPrefix) => normalizedRelativePath === excludedPrefix || normalizedRelativePath.startsWith(`${excludedPrefix}/`));
70126
+ });
70127
+ };
69794
70128
  var Config = class Config extends Service()("react-doctor/Config") {
69795
70129
  static layerNode = effect(Config, gen(function* () {
69796
70130
  const cache = yield* make$3({
@@ -69852,6 +70186,7 @@ var Files = class Files extends Service()("react-doctor/Files") {
69852
70186
  static layerNode = succeed$3(Files, Files.of({
69853
70187
  readLines: (input) => sync(() => createNodeReadFileLinesSync(input.rootDirectory)(input.filePath)),
69854
70188
  listSourceFiles: (rootDirectory) => sync(() => listSourceFiles(rootDirectory)),
70189
+ listSourceFilesCooperative: (input) => promise(() => listSourceFilesCooperative(input.rootDirectory, input.signal)),
69855
70190
  isFile: (filePath) => sync(() => isFile(filePath)),
69856
70191
  isDirectory: (filePath) => sync(() => isDirectory(filePath))
69857
70192
  }));
@@ -69878,6 +70213,16 @@ var Files = class Files extends Service()("react-doctor/Files") {
69878
70213
  }
69879
70214
  return files;
69880
70215
  }),
70216
+ listSourceFilesCooperative: (input) => sync(() => {
70217
+ const rootDirectory = input.rootDirectory;
70218
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
70219
+ const files = [];
70220
+ for (const absolute of tree.keys()) {
70221
+ if (!absolute.startsWith(prefix)) continue;
70222
+ files.push(absolute.slice(prefix.length));
70223
+ }
70224
+ return files;
70225
+ }),
69881
70226
  isFile: (filePath) => sync(() => tree.has(filePath)),
69882
70227
  isDirectory: (filePath) => sync(() => {
69883
70228
  const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
@@ -70342,7 +70687,7 @@ var Git = class Git extends Service()("react-doctor/Git") {
70342
70687
  changedFiles: yield* mergeUntracked(directory, splitNullSeparated(diff.stdout), includeUntracked),
70343
70688
  isCurrentChanges: false
70344
70689
  };
70345
- }).pipe(withSpan("Git.diffSelection")),
70690
+ }).pipe(catchReasons("ReactDoctorError", { GitInvocationFailed: () => succeed$2(null) }, (_reason, error) => fail$4(error)), withSpan("Git.diffSelection")),
70346
70691
  stagedFilePaths: (directory) => runGit(directory, [
70347
70692
  "diff",
70348
70693
  "--no-ext-diff",
@@ -70453,7 +70798,7 @@ const batchIncludePaths = (baseArgs, includePaths) => {
70453
70798
  for (const filePath of includePaths) {
70454
70799
  const entryLength = filePath.length + 1;
70455
70800
  const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
70456
- const exceedsFileCount = currentBatch.length >= 100;
70801
+ const exceedsFileCount = currentBatch.length >= 200;
70457
70802
  if (exceedsArgLength || exceedsFileCount) {
70458
70803
  batches.push(currentBatch);
70459
70804
  currentBatch = [];
@@ -70558,6 +70903,33 @@ const detectUserLintConfigPaths = (rootDirectory) => {
70558
70903
  return [];
70559
70904
  };
70560
70905
  const DISABLE_DIRECTIVE_PATTERN = /(eslint|oxlint)-disable/;
70906
+ const neutralizedFileLeases = /* @__PURE__ */ new Map();
70907
+ const activeNeutralizationInitializations = /* @__PURE__ */ new Set();
70908
+ const directoriesOverlap = (left, right) => {
70909
+ const isWithin = (parent, candidate) => {
70910
+ const relativePath = Path.relative(parent, candidate);
70911
+ return relativePath.length === 0 || !relativePath.startsWith(`..${Path.sep}`) && relativePath !== ".." && !Path.isAbsolute(relativePath);
70912
+ };
70913
+ return isWithin(left, right) || isWithin(right, left);
70914
+ };
70915
+ const withOverlappingInitializationLock = async (rootDirectory, operation) => {
70916
+ const blockers = [...activeNeutralizationInitializations].filter((initialization) => directoriesOverlap(initialization.rootDirectory, rootDirectory)).map((initialization) => initialization.done);
70917
+ let release;
70918
+ const initialization = {
70919
+ rootDirectory,
70920
+ done: new Promise((resolve) => {
70921
+ release = resolve;
70922
+ })
70923
+ };
70924
+ activeNeutralizationInitializations.add(initialization);
70925
+ await Promise.all(blockers);
70926
+ try {
70927
+ return await operation();
70928
+ } finally {
70929
+ activeNeutralizationInitializations.delete(initialization);
70930
+ release();
70931
+ }
70932
+ };
70561
70933
  const findFilesWithDisableDirectivesViaGit = async (rootDirectory, includePaths) => {
70562
70934
  const program = gen(function* () {
70563
70935
  return yield* (yield* Git).grep({
@@ -70600,34 +70972,84 @@ const findFilesWithDisableDirectivesViaFilesystem = (rootDirectory, includePaths
70600
70972
  };
70601
70973
  const findFilesWithDisableDirectives = async (rootDirectory, includePaths) => await findFilesWithDisableDirectivesViaGit(rootDirectory, includePaths) ?? findFilesWithDisableDirectivesViaFilesystem(rootDirectory, includePaths);
70602
70974
  const neutralizeContent = (content) => content.replaceAll("eslint-disable", "eslint_disable").replaceAll("oxlint-disable", "oxlint_disable");
70603
- const neutralizeDisableDirectives = async (rootDirectory, includePaths) => {
70604
- const filePaths = await findFilesWithDisableDirectives(rootDirectory, includePaths);
70605
- const originalContents = /* @__PURE__ */ new Map();
70975
+ const collectNeutralizationCandidatePaths = (rootDirectory, discoveredRelativePaths, includePaths) => {
70976
+ const resolvedRootDirectory = NFS.realpathSync(rootDirectory);
70977
+ const requestedPaths = includePaths && includePaths.length > 0 ? new Set(includePaths.flatMap((includePath) => {
70978
+ try {
70979
+ return [NFS.realpathSync(Path.resolve(resolvedRootDirectory, includePath))];
70980
+ } catch {
70981
+ return [];
70982
+ }
70983
+ })) : null;
70984
+ const candidatePaths = /* @__PURE__ */ new Set();
70985
+ for (const relativePath of discoveredRelativePaths) try {
70986
+ candidatePaths.add(NFS.realpathSync(Path.resolve(resolvedRootDirectory, relativePath)));
70987
+ } catch {
70988
+ continue;
70989
+ }
70990
+ for (const leasedPath of neutralizedFileLeases.keys()) {
70991
+ const relativePath = Path.relative(resolvedRootDirectory, leasedPath);
70992
+ if (!(relativePath.length === 0 || !relativePath.startsWith(`..${Path.sep}`) && relativePath !== ".." && !Path.isAbsolute(relativePath)) || requestedPaths && !requestedPaths.has(leasedPath)) continue;
70993
+ candidatePaths.add(leasedPath);
70994
+ }
70995
+ return candidatePaths;
70996
+ };
70997
+ const neutralizeDisableDirectives = async (rootDirectory, includePaths, options = {}) => {
70998
+ const resolvedRootDirectory = NFS.realpathSync(rootDirectory);
70999
+ const leasedPaths = /* @__PURE__ */ new Set();
70606
71000
  let isRestored = false;
70607
71001
  const restore = () => {
70608
71002
  if (isRestored) return;
70609
71003
  isRestored = true;
70610
- for (const [absolutePath, originalContent] of originalContents) try {
70611
- NFS.writeFileSync(absolutePath, originalContent);
70612
- } catch (error) {
70613
- process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
71004
+ for (const absolutePath of leasedPaths) {
71005
+ const lease = neutralizedFileLeases.get(absolutePath);
71006
+ if (!lease) continue;
71007
+ lease.referenceCount -= 1;
71008
+ if (lease.referenceCount > 0) continue;
71009
+ neutralizedFileLeases.delete(absolutePath);
71010
+ try {
71011
+ NFS.writeFileSync(absolutePath, lease.originalContent);
71012
+ } catch (error) {
71013
+ process.stderr.write(`[react-doctor] Failed to restore inline disable directives in ${absolutePath}: ${messageFromUnknown(error)}\n[react-doctor] Run: git checkout -- ${absolutePath}\n`);
71014
+ }
70614
71015
  }
70615
71016
  };
70616
71017
  const onExit = () => restore();
70617
71018
  process.once("exit", onExit);
70618
- for (const relativePath of filePaths) {
70619
- const absolutePath = Path.join(rootDirectory, relativePath);
71019
+ const leaseCandidatePath = (absolutePath) => {
71020
+ if (leasedPaths.has(absolutePath)) return;
71021
+ const existingLease = neutralizedFileLeases.get(absolutePath);
71022
+ if (existingLease) {
71023
+ existingLease.referenceCount += 1;
71024
+ leasedPaths.add(absolutePath);
71025
+ return;
71026
+ }
70620
71027
  let originalContent;
70621
71028
  try {
70622
71029
  originalContent = NFS.readFileSync(absolutePath, "utf-8");
70623
71030
  } catch {
70624
- continue;
71031
+ return;
70625
71032
  }
70626
71033
  const neutralizedContent = neutralizeContent(originalContent);
70627
71034
  if (neutralizedContent !== originalContent) {
70628
- originalContents.set(absolutePath, originalContent);
70629
71035
  NFS.writeFileSync(absolutePath, neutralizedContent);
71036
+ neutralizedFileLeases.set(absolutePath, {
71037
+ originalContent,
71038
+ referenceCount: 1
71039
+ });
71040
+ leasedPaths.add(absolutePath);
70630
71041
  }
71042
+ };
71043
+ try {
71044
+ await withOverlappingInitializationLock(resolvedRootDirectory, async () => {
71045
+ for (const absolutePath of collectNeutralizationCandidatePaths(resolvedRootDirectory, [], includePaths)) if (neutralizedFileLeases.has(absolutePath)) leaseCandidatePath(absolutePath);
71046
+ const discoveredRelativePaths = await (options.findFiles ?? findFilesWithDisableDirectives)(resolvedRootDirectory, includePaths);
71047
+ for (const absolutePath of collectNeutralizationCandidatePaths(resolvedRootDirectory, discoveredRelativePaths, includePaths)) leaseCandidatePath(absolutePath);
71048
+ });
71049
+ } catch (error) {
71050
+ restore();
71051
+ process.removeListener("exit", onExit);
71052
+ throw error;
70631
71053
  }
70632
71054
  return () => {
70633
71055
  restore();
@@ -70800,7 +71222,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
70800
71222
  }
70801
71223
  return enabled;
70802
71224
  };
70803
- const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), includedTags = /* @__PURE__ */ new Set(), includeTagDefaults = false, runtimeGlobals, unpluginAutoImportGlobalScopes, serverAuthFunctionNames, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
71225
+ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, extendsPaths = [], ignoredTags = /* @__PURE__ */ new Set(), includedTags = /* @__PURE__ */ new Set(), includeTagDefaults = false, runtimeGlobals, unpluginAutoImportGlobalScopes, serverAuthFunctionNames, projectIndexModuleSources, severityControls, userPlugins = [], disableReactHooksJsPlugin = false, ruleSelection, sidecarRuleIdFilter }) => {
70804
71226
  const hasIncludedTags = includedTags.size > 0;
70805
71227
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" || hasIncludedTags ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
70806
71228
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
@@ -70864,7 +71286,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
70864
71286
  }))
70865
71287
  } : {},
70866
71288
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
70867
- ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
71289
+ ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {},
71290
+ ...projectIndexModuleSources !== void 0 ? { projectIndexModuleSources: [...projectIndexModuleSources] } : {}
70868
71291
  } },
70869
71292
  rules: {
70870
71293
  ...reactCompilerRules,
@@ -71113,11 +71536,17 @@ const collectUnpluginAutoImportGlobalScopes = ({ rootDirectory, candidateFiles }
71113
71536
  return scopes.some((scope) => scope.names.length > 0) ? scopes : [];
71114
71537
  };
71115
71538
  const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
71539
+ const decodeDiagnostic = (raw) => {
71540
+ const diagnostic = validateDiagnostic$1(raw);
71541
+ return {
71542
+ ...diagnostic,
71543
+ relatedLocations: diagnostic.relatedLocations?.map((location) => ({ ...location }))
71544
+ };
71545
+ };
71116
71546
  const decodeFileDiagnostics = (raw) => {
71117
71547
  if (!Array.isArray(raw)) return null;
71118
71548
  try {
71119
- for (const entry of raw) validateDiagnostic$1(entry);
71120
- return raw;
71549
+ return raw.map((entry) => decodeDiagnostic(entry));
71121
71550
  } catch {
71122
71551
  return null;
71123
71552
  }
@@ -72349,6 +72778,12 @@ const buildProfiledNodeArguments = (input) => [
72349
72778
  ...input.heapProfileDirectory ? ["--heap-prof", `--heap-prof-dir=${input.heapProfileDirectory}`] : [],
72350
72779
  ...input.argumentsList
72351
72780
  ];
72781
+ const lowerChildProcessPriority = (processId) => {
72782
+ if (processId === void 0) return;
72783
+ try {
72784
+ OS.setPriority(processId, OS.constants.priority.PRIORITY_BELOW_NORMAL);
72785
+ } catch {}
72786
+ };
72352
72787
  const SANITIZED_ENV = buildOxlintChildEnv(process.env);
72353
72788
  /**
72354
72789
  * Spawn one oxlint subprocess with hard ceilings on wall time and
@@ -72387,6 +72822,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
72387
72822
  "pipe"
72388
72823
  ]
72389
72824
  });
72825
+ lowerChildProcessPriority(child.pid);
72390
72826
  const onAbort = () => {
72391
72827
  child.kill("SIGKILL");
72392
72828
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
@@ -72509,7 +72945,7 @@ const isParallelismRelatedSpawnError = (error) => {
72509
72945
  * loop with a slimmer config in that case.
72510
72946
  */
72511
72947
  const spawnLintBatches = async (input) => {
72512
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
72948
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 9, signal, deadlineEpochMs } = input;
72513
72949
  const resolveSourcePath = (filePath) => {
72514
72950
  const absoluteFilePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory, filePath);
72515
72951
  return sourcePathByLintPath?.get(Path.normalize(absoluteFilePath)) ?? filePath;
@@ -72517,6 +72953,12 @@ const spawnLintBatches = async (input) => {
72517
72953
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
72518
72954
  let rescueDeadlineEpochMs;
72519
72955
  const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
72956
+ const resolveSpawnTimeoutMs = () => {
72957
+ const deadlineBudgets = [deadlineEpochMs, rescueDeadlineEpochMs].filter((deadline) => deadline !== void 0).map(remainingDeadlineBudgetMs);
72958
+ if (deadlineBudgets.length === 0) return spawnTimeoutMs;
72959
+ const remainingDeadlineMs = Math.min(...deadlineBudgets);
72960
+ return spawnTimeoutMs === void 0 ? remainingDeadlineMs : Math.min(spawnTimeoutMs, remainingDeadlineMs);
72961
+ };
72520
72962
  const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
72521
72963
  const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
72522
72964
  const allDiagnostics = [];
@@ -72534,12 +72976,13 @@ const spawnLintBatches = async (input) => {
72534
72976
  const batchArgs = [...baseArgs, ...batch];
72535
72977
  try {
72536
72978
  const spawnBatch = () => {
72537
- if (isPastDeadline()) {
72979
+ const effectiveSpawnTimeoutMs = resolveSpawnTimeoutMs();
72980
+ if (effectiveSpawnTimeoutMs === 0) {
72538
72981
  deadlineSkippedFiles.push(...batch);
72539
72982
  batchState.deadlineSkippedFileCount += batch.length;
72540
72983
  return Promise.resolve(null);
72541
72984
  }
72542
- return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
72985
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, effectiveSpawnTimeoutMs, outputMaxBytes, signal, () => {
72543
72986
  if (batchState.didStart) return;
72544
72987
  batchState.didStart = true;
72545
72988
  startedFileCount += batchState.initialFileCount;
@@ -72694,11 +73137,36 @@ const validateRuleRegistration = () => {
72694
73137
  ].filter((entry) => entry !== null).join("; ");
72695
73138
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
72696
73139
  };
73140
+ const PROJECT_INDEX_MODULE_SOURCES = [
73141
+ "next/og",
73142
+ "@vercel/og",
73143
+ "satori",
73144
+ "remotion"
73145
+ ];
73146
+ const collectProjectIndexModuleSources = async (rootDirectory, candidateFiles) => {
73147
+ const foundModuleSources = /* @__PURE__ */ new Set();
73148
+ let sliceStartedAt = performance.now();
73149
+ for (const candidateFile of candidateFiles) {
73150
+ let sourceBuffer;
73151
+ try {
73152
+ sourceBuffer = NFS.readFileSync(Path.isAbsolute(candidateFile) ? candidateFile : Path.resolve(rootDirectory, candidateFile));
73153
+ } catch {
73154
+ continue;
73155
+ }
73156
+ for (const moduleSource of PROJECT_INDEX_MODULE_SOURCES) if (sourceBuffer.includes(`"${moduleSource}"`) || sourceBuffer.includes(`'${moduleSource}'`)) foundModuleSources.add(moduleSource);
73157
+ if (foundModuleSources.size === PROJECT_INDEX_MODULE_SOURCES.length) break;
73158
+ if (performance.now() - sliceStartedAt >= 12) {
73159
+ await yieldToEventLoop();
73160
+ sliceStartedAt = performance.now();
73161
+ }
73162
+ }
73163
+ return [...foundModuleSources].sort();
73164
+ };
72697
73165
  /**
72698
73166
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
72699
- * `batchIncludePaths` greedily fills 100-file batches and leaves a small
72700
- * remainder batch (617 files → 6×100 + 17, so the parallel pool's wall clock
72701
- * is set by whichever 100-file chunk happened to collect the most work while
73167
+ * `batchIncludePaths` greedily fills fixed-size batches and leaves a small
73168
+ * remainder batch, so the parallel pool's wall clock is set by whichever
73169
+ * full chunk happened to collect the most work while
72702
73170
  * the remainder's worker idles), this planner keeps the SAME mandatory batch
72703
73171
  * count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
72704
73172
  * perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
@@ -72727,7 +73195,7 @@ const validateRuleRegistration = () => {
72727
73195
  */
72728
73196
  const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
72729
73197
  if (files.length === 0) return [];
72730
- const batchCount = Math.ceil(files.length / 100);
73198
+ const batchCount = Math.ceil(files.length / 200);
72731
73199
  const maxFilesPerBatch = Math.ceil(files.length / batchCount);
72732
73200
  const baseArgsLengthChars = estimateArgsLength(baseArgs);
72733
73201
  const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
@@ -73256,6 +73724,7 @@ const runOxlint = async (options) => {
73256
73724
  await yieldToEventLoop();
73257
73725
  const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
73258
73726
  const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
73727
+ const projectIndexModuleSources = includePaths === void 0 && options.deadlineEpochMs === void 0 ? await collectProjectIndexModuleSources(rootDirectory, candidateFiles) : void 0;
73259
73728
  unpluginAutoImportGlobalScopes = collectUnpluginAutoImportGlobalScopes({
73260
73729
  rootDirectory,
73261
73730
  candidateFiles
@@ -73278,6 +73747,7 @@ const runOxlint = async (options) => {
73278
73747
  runtimeGlobals,
73279
73748
  unpluginAutoImportGlobalScopes,
73280
73749
  serverAuthFunctionNames,
73750
+ projectIndexModuleSources,
73281
73751
  severityControls,
73282
73752
  userPlugins,
73283
73753
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
@@ -73745,10 +74215,10 @@ const translateProjectInfoError = (cause, directory) => {
73745
74215
  }) });
73746
74216
  };
73747
74217
  var Project = class Project extends Service()("react-doctor/Project") {
73748
- static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (directory) {
74218
+ static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (input) {
73749
74219
  return yield* try_({
73750
- try: () => discoverProject(directory),
73751
- catch: (cause) => translateProjectInfoError(cause, directory)
74220
+ try: () => discoverProject(input.directory, { sourceFileCount: input.sourceFileCount }),
74221
+ catch: (cause) => translateProjectInfoError(cause, input.directory)
73752
74222
  });
73753
74223
  }) }));
73754
74224
  static layerOf = (projectInfo) => succeed$3(Project, Project.of({ discover: () => succeed$2(projectInfo) }));
@@ -74333,9 +74803,10 @@ const checkSupplyChain = (input) => gen(function* () {
74333
74803
  * outage.
74334
74804
  */
74335
74805
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
74336
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
74337
- ...input,
74338
- totalTimeoutMs
74806
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (configuredTimeoutMs) => checkSupplyChain({
74807
+ rootDirectory: input.rootDirectory,
74808
+ userConfig: input.userConfig,
74809
+ totalTimeoutMs: input.timeoutMs ?? configuredTimeoutMs
74339
74810
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
74340
74811
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
74341
74812
  };
@@ -74370,10 +74841,6 @@ const NO_HOOKS = {
74370
74841
  beforeLint: () => void_,
74371
74842
  afterLint: () => void_
74372
74843
  };
74373
- const filterMapNullable = (transform) => fromPredicateOption((value) => {
74374
- const result = transform(value);
74375
- return result === null ? none() : some(result);
74376
- });
74377
74844
  const fileReader = (filesService, rootDirectory) => (filePath) => {
74378
74845
  const lines = runSync(filesService.readLines({
74379
74846
  filePath,
@@ -74397,7 +74864,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
74397
74864
  * The GitHub viewer-permission lookup is forked onto a background
74398
74865
  * fiber here and joined late (it feeds score metadata, not
74399
74866
  * diagnostics).
74400
- * 2. beforeLint hook (e.g. CLI renders the project-detection block)
74867
+ * 2. beforeLint hook (e.g. CLI records project telemetry)
74401
74868
  * 3. environment checks (reduced-motion + pnpm hardening +
74402
74869
  * expo/react-native), collected synchronously. The heavier
74403
74870
  * content-regex security scan is forked instead (like supply-chain
@@ -74410,13 +74877,11 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
74410
74877
  * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
74411
74878
  * socket can't drag out its join; on timeout it fails open to no
74412
74879
  * diagnostics — the same outcome class as a Socket outage.
74413
- * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
74414
- * fiber) ONLY when the memory gate has headroom to run the 8 GB
74415
- * dead-code child alongside the oxlint workers — or when overlap is
74416
- * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
74417
- * runs sequentially after lint, exactly as it did pre-overlap. The
74418
- * fiber is joined (or interrupted, SIGKILLing its worker, on lint
74419
- * failure) before diagnostics are concatenated. The afterLint hook
74880
+ * 5. Linter.run runs; DeadCode.run forks only when
74881
+ * REACT_DOCTOR_DEAD_CODE_OVERLAP=on. The default "auto" and explicit
74882
+ * "off" paths stay sequential. The fiber is joined (or interrupted,
74883
+ * SIGKILLing its worker, on lint failure) before diagnostics are
74884
+ * concatenated. The afterLint hook
74420
74885
  * fires between lint and dead-code. Progress spinner labels AND the
74421
74886
  * final diagnostic / score order stay independent of execution
74422
74887
  * order, so terminal output is identical either way; supply-chain
@@ -74451,8 +74916,28 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74451
74916
  const partialFailuresRef = yield* LintPartialFailures;
74452
74917
  const resolvedConfig = yield* configService.resolve(input.directory);
74453
74918
  const scanDirectory = resolvedConfig.resolvedDirectory;
74454
- const project = yield* projectService.discover(scanDirectory);
74919
+ const excludedProjectDirectories = (input.excludedProjectDirectories ?? []).map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, scanDirectory));
74920
+ const excludedProjectRelativePaths = excludedProjectDirectories.map((excludedDirectory) => Path.relative(scanDirectory, excludedDirectory).replaceAll("\\", "/"));
74921
+ const isExcludedProjectDiagnostic = (diagnostic) => {
74922
+ const relativeFilePath = toNormalizedRelativePath(diagnostic.filePath, scanDirectory);
74923
+ return excludedProjectRelativePaths.some((excludedRelativePath) => relativeFilePath === excludedRelativePath || relativeFilePath.startsWith(`${excludedRelativePath}/`));
74924
+ };
74925
+ const precomputedSourceFiles = input.suppressScanSummary === true || excludedProjectDirectories.length > 0 ? yield* filesService.listSourceFilesCooperative({
74926
+ rootDirectory: scanDirectory,
74927
+ signal: input.signal
74928
+ }) : null;
74929
+ const includedPrecomputedSourceFiles = precomputedSourceFiles !== null && excludedProjectDirectories.length > 0 ? filterPathsOutsideDirectories({
74930
+ rootDirectory: scanDirectory,
74931
+ relativePaths: precomputedSourceFiles,
74932
+ excludedDirectories: excludedProjectDirectories
74933
+ }) : precomputedSourceFiles;
74934
+ const sourceFileCount = excludedProjectDirectories.length > 0 ? includedPrecomputedSourceFiles?.length : input.precomputedSourceFileCount ?? includedPrecomputedSourceFiles?.length;
74935
+ const project = yield* projectService.discover({
74936
+ directory: scanDirectory,
74937
+ sourceFileCount
74938
+ });
74455
74939
  if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
74940
+ const deadCodeSourceFileCount = input.retainExcludedProjectDeadCodeDiagnostics === true && precomputedSourceFiles !== null ? precomputedSourceFiles.length : project.sourceFileCount;
74456
74941
  const [repo, sha, defaultBranch] = yield* all([
74457
74942
  gitService.githubRepo(scanDirectory).pipe(orElseSucceed(() => null)),
74458
74943
  gitService.headSha(scanDirectory).pipe(orElseSucceed(() => null)),
@@ -74463,11 +74948,16 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74463
74948
  directory: scanDirectory,
74464
74949
  repo
74465
74950
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
74466
- const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
74467
- const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
74951
+ let lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, includedPrecomputedSourceFiles ?? void 0);
74952
+ if (excludedProjectDirectories.length > 0) lintIncludePaths = filterPathsOutsideDirectories({
74953
+ rootDirectory: scanDirectory,
74954
+ relativePaths: lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory)),
74955
+ excludedDirectories: excludedProjectDirectories
74956
+ });
74957
+ const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
74468
74958
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
74469
74959
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
74470
- yield* beforeLint(project, lintIncludePaths ?? void 0);
74960
+ yield* beforeLint(project);
74471
74961
  const isDiffMode = input.includePaths.length > 0;
74472
74962
  const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? true;
74473
74963
  const transform = buildDiagnosticPipeline({
@@ -74477,7 +74967,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74477
74967
  respectInlineDisables: input.respectInlineDisables,
74478
74968
  showWarnings
74479
74969
  });
74480
- const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
74970
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(fromPredicateOption((diagnostic) => {
74971
+ const filteredDiagnostic = transform.apply(diagnostic);
74972
+ return filteredDiagnostic === null ? none() : some(filteredDiagnostic);
74973
+ })));
74481
74974
  const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
74482
74975
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
74483
74976
  ...checkReducedMotion(scanDirectory),
@@ -74487,17 +74980,31 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74487
74980
  ...checkReactNativeProject(scanDirectory, project)
74488
74981
  ])));
74489
74982
  const securityScanFailedRef = yield* make$13(false);
74490
- const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
74983
+ let didSecurityScanReachDeadline = false;
74984
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise((signal) => checkSecurityScanCooperative(scanDirectory, {
74491
74985
  project,
74492
74986
  ignoredTags: input.ignoredTags,
74493
74987
  includedTags: input.includedTags,
74494
- includeTagDefaults: input.includeTagDefaults
74988
+ includeTagDefaults: input.includeTagDefaults,
74989
+ excludedDirectories: new Set(excludedProjectDirectories),
74990
+ deadlineEpochMs: input.deadlineEpochMs,
74991
+ signal,
74992
+ onDeadlineExceeded: () => {
74993
+ didSecurityScanReachDeadline = true;
74994
+ }
74495
74995
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
74996
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
74997
+ const capOptionalToDeadline = (phaseTimeoutMs) => {
74998
+ if (input.deadlineEpochMs === void 0) return phaseTimeoutMs;
74999
+ const remainingBudgetMs = remainingDeadlineBudgetMs(input.deadlineEpochMs);
75000
+ return phaseTimeoutMs === null ? remainingBudgetMs : Math.min(phaseTimeoutMs, remainingBudgetMs);
75001
+ };
74496
75002
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
74497
- const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
75003
+ const supplyChainOverlapTimeout = capToDeadline(yield* SupplyChainOverlapTimeoutMs);
74498
75004
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
74499
75005
  rootDirectory: scanDirectory,
74500
- userConfig: resolvedConfig.config
75006
+ userConfig: resolvedConfig.config,
75007
+ timeoutMs: supplyChainOverlapTimeout
74501
75008
  }))).pipe(map$3((diagnostics) => ({
74502
75009
  diagnostics,
74503
75010
  timedOut: false
@@ -74521,46 +75028,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74521
75028
  const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
74522
75029
  const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
74523
75030
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
74524
- const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
74525
75031
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
74526
- const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
74527
75032
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
74528
75033
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
74529
75034
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
74530
75035
  const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
74531
75036
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
74532
- const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
74533
- rootDirectory: scanDirectory,
74534
- parseConcurrency: deadCodeParseConcurrency,
74535
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
74536
- onCacheOutcome: (didHitCache) => {
74537
- deadCodeCacheHit = didHitCache;
74538
- },
74539
- onSummaryCacheStats: (stats) => {
74540
- deadCodeSummaryCacheHits = stats.hits;
74541
- deadCodeSummaryCacheMisses = stats.misses;
74542
- }
74543
- }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
74544
- yield* set(deadCodeFailure, {
74545
- didFail: true,
74546
- reason: error.message
74547
- });
74548
- return empty$4;
74549
- })))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
74550
- onNone: () => set(deadCodeFailure, {
74551
- didFail: true,
74552
- reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
74553
- }).pipe(as([])),
74554
- onSome: succeed$2
74555
- })));
75037
+ const buildCollectDeadCode = (deadCodeTimeout) => {
75038
+ const collectDeadCode = runCollect(applyPerElementPipeline(deadCodeService.run({
75039
+ rootDirectory: scanDirectory,
75040
+ parseConcurrency: deadCodeParseConcurrency,
75041
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
75042
+ onCacheOutcome: (didHitCache) => {
75043
+ deadCodeCacheHit = didHitCache;
75044
+ },
75045
+ onSummaryCacheStats: (stats) => {
75046
+ deadCodeSummaryCacheHits = stats.hits;
75047
+ deadCodeSummaryCacheMisses = stats.misses;
75048
+ }
75049
+ }).pipe(filter((diagnostic) => input.retainExcludedProjectDeadCodeDiagnostics === true || !isExcludedProjectDiagnostic(diagnostic)), catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
75050
+ yield* set(deadCodeFailure, {
75051
+ didFail: true,
75052
+ reason: error.message
75053
+ });
75054
+ return empty$4;
75055
+ }))))));
75056
+ const phaseTimeoutMs = deadCodeTimeout.phaseTimeoutMs;
75057
+ if (phaseTimeoutMs === null) return collectDeadCode;
75058
+ return collectDeadCode.pipe(timeoutOption(phaseTimeoutMs), flatMap$2(match$3({
75059
+ onNone: () => set(deadCodeFailure, {
75060
+ didFail: true,
75061
+ reason: `Dead-code analysis exceeded ${Math.round(phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
75062
+ }).pipe(as([])),
75063
+ onSome: succeed$2
75064
+ })));
75065
+ };
74556
75066
  const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
74557
- sourceFileCount: project.sourceFileCount,
75067
+ sourceFileCount: deadCodeSourceFileCount,
74558
75068
  deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
74559
75069
  fullConcurrency: scanConcurrency
74560
75070
  });
74561
75071
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
74562
75072
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
74563
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
75073
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
74564
75074
  })) : null;
74565
75075
  const scanProgress = yield* progressService.start("Scanning...");
74566
75076
  const scanStartTime = Date.now();
@@ -74611,7 +75121,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74611
75121
  });
74612
75122
  return empty$4;
74613
75123
  }))));
74614
- const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
75124
+ const collectLintDiagnostics = runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream));
75125
+ const lintCollected = dedupeRelatedDiagnostics(yield* lintPhaseTimeoutMs === null ? collectLintDiagnostics : collectLintDiagnostics.pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
74615
75126
  onNone: () => set(lintFailure, {
74616
75127
  didFail: true,
74617
75128
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -74641,13 +75152,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74641
75152
  } else {
74642
75153
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
74643
75154
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
74644
- sourceFileCount: totalFileCount,
75155
+ sourceFileCount: input.retainExcludedProjectDeadCodeDiagnostics === true ? deadCodeSourceFileCount : totalFileCount,
74645
75156
  deadCodeConcurrency: scanConcurrency,
74646
75157
  fullConcurrency: scanConcurrency
74647
75158
  });
74648
75159
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
74649
75160
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
74650
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
75161
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
74651
75162
  });
74652
75163
  }
74653
75164
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -74691,7 +75202,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74691
75202
  metadata: scoreMetadata
74692
75203
  });
74693
75204
  const lintPartialFailures = yield* get$2(partialFailuresRef);
74694
- const securityScanFailed = yield* get$2(securityScanFailedRef);
75205
+ const didSecurityScanFail = yield* get$2(securityScanFailedRef);
75206
+ const securityScanFailed = didSecurityScanFail || didSecurityScanReachDeadline;
75207
+ let securityScanFailureReason = null;
75208
+ if (didSecurityScanReachDeadline) securityScanFailureReason = "Security scan reached the max scan duration; findings collected before the deadline were preserved.";
75209
+ else if (didSecurityScanFail) securityScanFailureReason = "Security scan failed and was skipped.";
74695
75210
  return {
74696
75211
  project,
74697
75212
  userConfig: resolvedConfig.config,
@@ -74714,6 +75229,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74714
75229
  scanConcurrency,
74715
75230
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
74716
75231
  securityScanFailed,
75232
+ securityScanFailureReason,
74717
75233
  lintCacheHitFileCount,
74718
75234
  lintCacheTotalFileCount,
74719
75235
  lintSidecarReplayedFileCount,
@@ -74729,7 +75245,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74729
75245
  "inspect.runDeadCode": input.runDeadCode,
74730
75246
  "inspect.isCi": input.isCi,
74731
75247
  "inspect.scoreSurface": input.scoreSurface ?? "score"
74732
- } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
75248
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => {
75249
+ if (scanDeadlineMs === null) return scanProgram;
75250
+ return scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })));
75251
+ }));
74733
75252
  const parseNodeVersion = (versionString) => {
74734
75253
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
74735
75254
  return {
@@ -74830,16 +75349,6 @@ const resolveNodeForOxlint = () => {
74830
75349
  });
74831
75350
  });
74832
75351
  /**
74833
- * Zip-Slip defense: relative paths come from git (`diff --name-only`), which
74834
- * normalizes during ordinary adds, but a crafted index/pack/symlinked tree can
74835
- * smuggle `..` segments that escape the temp root. Resolve against the temp dir
74836
- * and reject anything that lands outside before writing.
74837
- */
74838
- const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
74839
- const relative = path.relative(parentAbsolutePath, childAbsolutePath);
74840
- return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
74841
- };
74842
- /**
74843
75352
  * Writes a set of source files (supplied by `readContent` — e.g.
74844
75353
  * `git show <ref>:<path>` for a baseline tree, or `git show :<path>` for the
74845
75354
  * index) into a temp tree that mirrors the project layout, copying the
@@ -74875,7 +75384,7 @@ const materializeSourceTree = (input) => gen(function* () {
74875
75384
  const sourcePath = path.join(input.directory, subdirectory, configFilename);
74876
75385
  const targetPath = path.resolve(resolvedTempDirectory, subdirectory, configFilename);
74877
75386
  if (!isPathInsideDirectory(targetPath, resolvedTempDirectory)) continue;
74878
- if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) {
75387
+ if (isFile(sourcePath) && !fs.existsSync(targetPath)) {
74879
75388
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
74880
75389
  fs.cpSync(sourcePath, targetPath);
74881
75390
  }
@@ -75016,14 +75525,6 @@ const runEditorScan = async (input) => {
75016
75525
  error: messageFromUnknown(error)
75017
75526
  };
75018
75527
  };
75019
- /**
75020
- * Back-compat alias: the streaming pipeline holds its caches in
75021
- * per-pipeline closures that are garbage-collected when the pipeline
75022
- * goes out of scope, so there is nothing to clear at module scope. The
75023
- * public CLI's `clearCaches()` still calls this for symmetry with the
75024
- * other `clear*` helpers.
75025
- */
75026
- const clearAutoSuppressionCaches = () => {};
75027
75528
  const lookupOwnRule = (rule) => Object.hasOwn(REACT_DOCTOR_RULE_REGISTRY, rule) ? REACT_DOCTOR_RULE_REGISTRY[rule] : void 0;
75028
75529
  /**
75029
75530
  * Returns presentation metadata for `<plugin>/<rule>`, or `null` when the
@@ -75063,7 +75564,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
75063
75564
  /** Display name used in client-facing messages and progress titles. */
75064
75565
  const SERVER_DISPLAY_NAME = "React Doctor";
75065
75566
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
75066
- const SERVER_VERSION = "0.9.3";
75567
+ const SERVER_VERSION = "0.9.4";
75067
75568
  /** `Diagnostic.source` shown next to every published diagnostic. */
75068
75569
  const DIAGNOSTIC_SOURCE = "react-doctor";
75069
75570
  /**
@@ -75691,10 +76192,7 @@ const createProjectGraph = (options) => {
75691
76192
  for (const root of roots) try {
75692
76193
  for (const workspacePackage of discoverReactSubprojects(root)) {
75693
76194
  const directory = normalizeDirectory(workspacePackage.directory);
75694
- if (!seen.has(directory)) seen.set(directory, {
75695
- directory,
75696
- name: workspacePackage.name
75697
- });
76195
+ if (!seen.has(directory)) seen.set(directory, { directory });
75698
76196
  }
75699
76197
  } catch (error) {
75700
76198
  logger.warn(`Project discovery failed for ${root}: ${messageFromUnknown(error)}`);
@@ -75719,8 +76217,8 @@ const createProjectGraph = (options) => {
75719
76217
  clearProjectCache();
75720
76218
  clearConfigCache();
75721
76219
  clearPackageJsonCache();
76220
+ clearPackageRoleCache();
75722
76221
  clearIgnorePatternsCache();
75723
- clearAutoSuppressionCaches();
75724
76222
  clearMinifiedFileCache();
75725
76223
  projects = null;
75726
76224
  }
@@ -76116,11 +76614,9 @@ const createScheduler = (options) => {
76116
76614
  timers.clear();
76117
76615
  queue.length = 0;
76118
76616
  };
76119
- const pendingCount = () => queue.length + running + timers.size;
76120
76617
  return {
76121
76618
  enqueue,
76122
76619
  cancelProject,
76123
- pendingCount,
76124
76620
  dispose
76125
76621
  };
76126
76622
  };
@@ -76767,8 +77263,11 @@ const METRIC = {
76767
77263
  scanPhaseDuration: "scan.phase_duration",
76768
77264
  scanFiles: "scan.files",
76769
77265
  scanScore: "scan.score",
77266
+ scanScoreRetry: "scan.score_retry",
76770
77267
  scanClean: "scan.clean",
76771
77268
  scanCheckSkipped: "scan.check_skipped",
77269
+ scanProjectSkipped: "scan.project_skipped",
77270
+ scanWorkspaceDeadCodeShared: "scan.workspace_deadcode_shared",
76772
77271
  scanNoReactDetected: "scan.no_react_detected",
76773
77272
  baselineDegraded: "baseline.degraded",
76774
77273
  ruleFired: "rule.fired",
@@ -76795,9 +77294,14 @@ const METRIC = {
76795
77294
  lspScanCompleted: "lsp.scan.completed",
76796
77295
  lspScanDuration: "lsp.scan.duration",
76797
77296
  lspScanDiagnostics: "lsp.scan.diagnostics",
76798
- tuiActionMenuOpened: "tui.action_menu_opened",
76799
77297
  tuiCompactReportShown: "tui.compact_report_shown",
76800
77298
  tuiFindingNavigated: "tui.finding_navigated",
77299
+ tuiIssueStreamShown: "tui.issue_stream_shown",
77300
+ tuiProjectPathContextShown: "tui.project_path_context_shown",
77301
+ tuiProjectSelectInlineShown: "tui.project_select_inline_shown",
77302
+ tuiReportActionSelected: "tui.report_action_selected",
77303
+ tuiCancelled: "tui.cancelled",
77304
+ tuiScanInlineShown: "tui.scan_inline_shown",
76801
77305
  tuiStackedReportCapped: "tui.stacked_report_capped",
76802
77306
  aiTrainingWarningShown: "ai.training.warning_shown",
76803
77307
  jsonOutUsed: "json.out_used"
@@ -76812,6 +77316,9 @@ const METRIC = {
76812
77316
  */
76813
77317
  const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
76814
77318
  //#endregion
77319
+ //#region src/cli/utils/is-env-flag-enabled.ts
77320
+ const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
77321
+ //#endregion
76815
77322
  //#region src/cli/utils/anonymize-text.ts
76816
77323
  /**
76817
77324
  * Free-text fields can carry both a home-directory path (the OS username) and a
@@ -76972,7 +77479,6 @@ const resolveTracesSampleRate = () => {
76972
77479
  * session.
76973
77480
  */
76974
77481
  let lspTracesSampleRate = 0;
76975
- const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
76976
77482
  const nodeMajorVersion = () => Number.parseInt(process.versions.node.split(".", 1)[0] ?? "", 10) || 0;
76977
77483
  const shouldEnableLspTelemetry = () => {
76978
77484
  if (isEnvFlagEnabled(process.env.REACT_DOCTOR_NO_TELEMETRY)) return false;
@@ -77100,5 +77606,5 @@ const startLanguageServer = () => {
77100
77606
  };
77101
77607
  //#endregion
77102
77608
  export { startLanguageServer };
77103
- !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]="a07beba2-2d2e-59f0-8edb-430554d791d1")}catch(e){}}();
77104
- //# debugId=a07beba2-2d2e-59f0-8edb-430554d791d1
77609
+ !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]="8e8ec622-f2dc-5f52-bfe8-232b07051a7f")}catch(e){}}();
77610
+ //# debugId=8e8ec622-f2dc-5f52-bfe8-232b07051a7f