react-doctor 0.9.3 → 0.9.4-dev.8b97fdc

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
  /**
@@ -15586,7 +15586,7 @@ function transformOptional(f) {
15586
15586
  */
15587
15587
  function withDefault$1(defaultValue) {
15588
15588
  return new Getter((o) => {
15589
- const filtered = filter(o, isNotUndefined);
15589
+ const filtered = filter$1(o, isNotUndefined);
15590
15590
  return isSome(filtered) ? succeed$2(filtered) : mapEager(defaultValue, some);
15591
15591
  });
15592
15592
  }
@@ -19434,6 +19434,10 @@ var JsonReportProjectEntryV3 = class extends Class("JsonReportProjectEntryV3")({
19434
19434
  scannedFileCount: optional(Number$1),
19435
19435
  elapsedMilliseconds: Number$1
19436
19436
  }) {};
19437
+ var JsonReportSkippedProject = class extends Class("JsonReportSkippedProject")({
19438
+ directory: String$1,
19439
+ reason: Literal("max-duration")
19440
+ }) {};
19437
19441
  /** Original full, diff, and staged report contract. */
19438
19442
  var JsonReportV1 = class extends Class("JsonReportV1")({
19439
19443
  schemaVersion: Literal(1),
@@ -19462,6 +19466,7 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
19462
19466
  reactDetected: optional(Boolean$1),
19463
19467
  diff: NullOr(JsonReportDiffInfo),
19464
19468
  projects: ArraySchema(JsonReportProjectEntry),
19469
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19465
19470
  diagnostics: ArraySchema(Diagnostic),
19466
19471
  summary: JsonReportSummary,
19467
19472
  elapsedMilliseconds: Number$1,
@@ -19490,6 +19495,7 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
19490
19495
  diff: NullOr(JsonReportDiffInfo),
19491
19496
  baseline: JsonReportBaseline,
19492
19497
  projects: ArraySchema(JsonReportProjectEntry),
19498
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19493
19499
  diagnostics: ArraySchema(Diagnostic),
19494
19500
  summary: JsonReportSummary,
19495
19501
  elapsedMilliseconds: Number$1,
@@ -19506,6 +19512,7 @@ var JsonReportV3 = class extends Class("JsonReportV3")({
19506
19512
  diff: NullOr(JsonReportDiffInfo),
19507
19513
  baseline: optional(JsonReportBaseline),
19508
19514
  projects: ArraySchema(JsonReportProjectEntryV3),
19515
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19509
19516
  diagnostics: ArraySchema(JsonReportDiagnosticV3),
19510
19517
  summary: JsonReportSummary,
19511
19518
  elapsedMilliseconds: Number$1,
@@ -53488,6 +53495,47 @@ const flattenArray = (self) => transformPull$1(self, (pull) => {
53488
53495
  }));
53489
53496
  });
53490
53497
  /**
53498
+ * Filters arrays of elements emitted by a channel, applying the filter
53499
+ * to each element within the arrays and only emitting non-empty filtered arrays.
53500
+ *
53501
+ * **Example** (Filtering array output)
53502
+ *
53503
+ * ```ts
53504
+ * import { Array, Channel } from "effect"
53505
+ *
53506
+ * const nonEmptyArrayPredicate = Array.isReadonlyArrayNonEmpty
53507
+ *
53508
+ * // Create a channel that outputs arrays of mixed data
53509
+ * const arrayChannel = Channel.fromIterable([
53510
+ * Array.make(1, 2, 3, 4, 5),
53511
+ * Array.make(6, 7, 8, 9, 10),
53512
+ * Array.make(11, 12, 13, 14, 15)
53513
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
53514
+ *
53515
+ * // Filter arrays to keep only even numbers
53516
+ * const evenArraysChannel = Channel.filterArray(arrayChannel, (n) => n % 2 === 0)
53517
+ * // Outputs: [2, 4], [6, 8, 10], [12, 14]
53518
+ * // Note: Only non-empty filtered arrays are emitted
53519
+ *
53520
+ * // Arrays that would become empty after filtering are discarded entirely
53521
+ * const oddChannel = Channel.fromIterable([
53522
+ * Array.make(1, 3, 5),
53523
+ * Array.make(2, 4),
53524
+ * Array.make(7, 9)
53525
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
53526
+ * const filteredOddChannel = Channel.filterArray(oddChannel, (n) => n % 2 === 0)
53527
+ * // Outputs: [2, 4] (the arrays [1,3,5] and [7,9] are discarded)
53528
+ * ```
53529
+ *
53530
+ * @category filtering
53531
+ * @since 4.0.0
53532
+ */
53533
+ const filterArray = /* @__PURE__ */ dual(2, (self, predicate) => transformPull$1(self, (pull) => succeed$2(flatMap$2(pull, function loop(arr) {
53534
+ const passes = [];
53535
+ for (let i = 0; i < arr.length; i++) if (predicate(arr[i])) passes.push(arr[i]);
53536
+ return isReadonlyArrayNonEmpty(passes) ? succeed$2(passes) : flatMap$2(pull, loop);
53537
+ }))));
53538
+ /**
53491
53539
  * Filters and maps each element inside emitted non-empty arrays using a
53492
53540
  * `Filter`.
53493
53541
  *
@@ -55125,6 +55173,30 @@ const concat = /* @__PURE__ */ dual(2, (self, that) => flatten(fromArray([self,
55125
55173
  */
55126
55174
  const merge$1 = /* @__PURE__ */ dual((args) => isStream(args[0]) && isStream(args[1]), (self, that, options) => fromChannel(merge$2(toChannel(self), toChannel(that), options)));
55127
55175
  /**
55176
+ * Filters a stream to the elements that satisfy a predicate.
55177
+ *
55178
+ * **Example** (Filtering stream values)
55179
+ *
55180
+ * ```ts
55181
+ * import { Console, Effect, Stream } from "effect"
55182
+ *
55183
+ * const program = Effect.gen(function*() {
55184
+ * const stream = Stream.make(1, 2, 3, 4).pipe(
55185
+ * Stream.filter((n) => n % 2 === 0)
55186
+ * )
55187
+ * const values = yield* Stream.runCollect(stream)
55188
+ * yield* Console.log(values)
55189
+ * })
55190
+ *
55191
+ * Effect.runPromise(program)
55192
+ * // Output: [ 2, 4 ]
55193
+ * ```
55194
+ *
55195
+ * @category filtering
55196
+ * @since 2.0.0
55197
+ */
55198
+ const filter = /* @__PURE__ */ dual(2, (self, predicate) => fromChannel(filterArray(toChannel(self), predicate)));
55199
+ /**
55128
55200
  * Filters and maps stream elements in one pass using a `Filter`.
55129
55201
  *
55130
55202
  * @category filtering
@@ -61908,6 +61980,7 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
61908
61980
  const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
61909
61981
  const MINIFIED_SNIFF_BYTES = 65536;
61910
61982
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
61983
+ const GIT_CHECK_ATTR_MAX_BUFFER_BYTES = GIT_LS_FILES_MAX_BUFFER_BYTES;
61911
61984
  const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
61912
61985
  const IGNORED_DIRECTORIES = new Set([
61913
61986
  ".angular",
@@ -62216,13 +62289,9 @@ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
62216
62289
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
62217
62290
  const ABORT_EXIT_CODES = new Set([134, 3221226505]);
62218
62291
  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
62292
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
62223
62293
  const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
62224
62294
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
62225
- const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
62226
62295
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
62227
62296
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
62228
62297
  const REACT_SERVER_DOM_PACKAGES = [
@@ -62269,7 +62338,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
62269
62338
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
62270
62339
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
62271
62340
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
62272
- const CORE_PACKAGE_VERSION = "0.9.3";
62341
+ const CORE_PACKAGE_VERSION = "0.9.4";
62273
62342
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
62274
62343
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
62275
62344
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -62317,6 +62386,66 @@ const isPlainObject = (value) => {
62317
62386
  const prototype = Object.getPrototypeOf(value);
62318
62387
  return prototype === null || prototype === Object.prototype;
62319
62388
  };
62389
+ const LINGUIST_ATTRIBUTES = ["linguist-generated", "linguist-vendored"];
62390
+ const DISABLED_ATTRIBUTE_VALUES = new Set([
62391
+ "unspecified",
62392
+ "unset",
62393
+ "false",
62394
+ "0",
62395
+ "off",
62396
+ "no"
62397
+ ]);
62398
+ const buildInput = (relativePaths) => `${relativePaths.join("\0")}\0`;
62399
+ const parseIgnoredPaths = (output) => {
62400
+ const ignoredPaths = /* @__PURE__ */ new Set();
62401
+ const fields = output.split("\0");
62402
+ for (let fieldIndex = 0; fieldIndex + 2 < fields.length; fieldIndex += 3) {
62403
+ const filePath = fields[fieldIndex];
62404
+ const value = fields[fieldIndex + 2]?.toLowerCase();
62405
+ if (filePath && value && !DISABLED_ATTRIBUTE_VALUES.has(value)) ignoredPaths.add(filePath);
62406
+ }
62407
+ return ignoredPaths;
62408
+ };
62409
+ const collectGitLinguistIgnoredPaths = (rootDirectory, relativePaths) => {
62410
+ if (relativePaths.length === 0) return /* @__PURE__ */ new Set();
62411
+ const result = spawnSync("git", [
62412
+ "check-attr",
62413
+ "-z",
62414
+ "--stdin",
62415
+ ...LINGUIST_ATTRIBUTES
62416
+ ], {
62417
+ cwd: rootDirectory,
62418
+ encoding: "utf-8",
62419
+ input: buildInput(relativePaths),
62420
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES
62421
+ });
62422
+ if (result.error || result.status !== 0) return /* @__PURE__ */ new Set();
62423
+ return parseIgnoredPaths(result.stdout);
62424
+ };
62425
+ const collectGitLinguistIgnoredPathsCooperative = (rootDirectory, relativePaths, signal) => {
62426
+ if (relativePaths.length === 0) return Promise.resolve(/* @__PURE__ */ new Set());
62427
+ return new Promise((resolve, reject) => {
62428
+ signal?.throwIfAborted();
62429
+ execFile("git", [
62430
+ "check-attr",
62431
+ "-z",
62432
+ "--stdin",
62433
+ ...LINGUIST_ATTRIBUTES
62434
+ ], {
62435
+ cwd: rootDirectory,
62436
+ encoding: "utf-8",
62437
+ killSignal: "SIGKILL",
62438
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES,
62439
+ signal
62440
+ }, (error, stdout) => {
62441
+ if (signal?.aborted) {
62442
+ reject(signal.reason);
62443
+ return;
62444
+ }
62445
+ resolve(error ? /* @__PURE__ */ new Set() : parseIgnoredPaths(stdout));
62446
+ }).stdin?.end(buildInput(relativePaths));
62447
+ });
62448
+ };
62320
62449
  const lastSourceMappingUrl = (fileContent) => {
62321
62450
  const lastReference = [...fileContent.matchAll(/\/\/# sourceMappingURL=(\S+)/g)].at(-1);
62322
62451
  return lastReference ? lastReference[1] : null;
@@ -62421,6 +62550,9 @@ function* walkSourceTreeFiles(rootDirectory) {
62421
62550
  }
62422
62551
  }
62423
62552
  }
62553
+ const yieldToEventLoop = () => new Promise((resolve) => {
62554
+ setImmediate(resolve);
62555
+ });
62424
62556
  const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
62425
62557
  const entries = [];
62426
62558
  for (const relativePath of relativePaths) {
@@ -62434,23 +62566,55 @@ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
62434
62566
  }
62435
62567
  return entries;
62436
62568
  };
62437
- const gitLsFiles = (rootDirectory, flags) => {
62569
+ const collectSizedSourceFilesCooperative = async (rootDirectory, relativePaths, signal) => {
62570
+ const entries = [];
62571
+ let sliceStartedAt = performance.now();
62572
+ for (const relativePath of relativePaths) {
62573
+ signal?.throwIfAborted();
62574
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
62575
+ const sizeBytes = statSourceFileSize(absolutePath);
62576
+ if (!isLargeMinifiedFile(absolutePath, sizeBytes)) entries.push({
62577
+ path: relativePath,
62578
+ sizeBytes: sizeBytes ?? 0
62579
+ });
62580
+ if (performance.now() - sliceStartedAt >= 12) {
62581
+ await yieldToEventLoop();
62582
+ sliceStartedAt = performance.now();
62583
+ }
62584
+ }
62585
+ return entries;
62586
+ };
62587
+ const parseGitSourceFilePaths = (output) => {
62588
+ const trackedPaths = /* @__PURE__ */ new Set();
62589
+ const untrackedPaths = [];
62590
+ for (const entry of output.split("\0")) {
62591
+ if (entry.length === 0) continue;
62592
+ const trackedPath = /^[0-7]{6} [0-9a-f]+ [0-3]\t([\s\S]+)$/.exec(entry)?.[1];
62593
+ if (trackedPath === void 0) untrackedPaths.push(entry);
62594
+ else trackedPaths.add(trackedPath);
62595
+ }
62596
+ return {
62597
+ trackedPaths: [...trackedPaths],
62598
+ untrackedPaths
62599
+ };
62600
+ };
62601
+ const listGitSourceFilePaths = (rootDirectory) => {
62438
62602
  const result = spawnSync("git", [
62439
62603
  "ls-files",
62440
62604
  "-z",
62441
- ...flags
62605
+ "--stage",
62606
+ "--others",
62607
+ "--exclude-standard"
62442
62608
  ], {
62443
62609
  cwd: rootDirectory,
62444
62610
  encoding: "utf-8",
62445
62611
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
62446
62612
  });
62447
62613
  if (result.error || result.status !== 0) return null;
62448
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0);
62614
+ return parseGitSourceFilePaths(result.stdout);
62449
62615
  };
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;
62616
+ const collectGitCandidateSourceFilePaths = (rootDirectory, paths) => {
62617
+ const { trackedPaths, untrackedPaths } = paths;
62454
62618
  const emitDuplicateJsPaths = collectTypeScriptEmitDuplicateJsPaths({
62455
62619
  trackedPaths: new Set(trackedPaths),
62456
62620
  untrackedPaths,
@@ -62458,12 +62622,65 @@ const listSourceFilesViaGit = (rootDirectory) => {
62458
62622
  });
62459
62623
  return [...trackedPaths, ...untrackedPaths].filter((filePath) => isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath) && !emitDuplicateJsPaths.has(filePath));
62460
62624
  };
62625
+ const filterGitSourceFilePaths = (rootDirectory, paths) => {
62626
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
62627
+ const linguistIgnoredPaths = collectGitLinguistIgnoredPaths(rootDirectory, candidatePaths);
62628
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
62629
+ };
62630
+ const listSourceFilesViaGit = (rootDirectory) => {
62631
+ const paths = listGitSourceFilePaths(rootDirectory);
62632
+ return paths === null ? null : filterGitSourceFilePaths(rootDirectory, paths);
62633
+ };
62634
+ const listSourceFilesViaGitCooperative = async (rootDirectory, signal) => {
62635
+ const paths = await new Promise((resolve, reject) => {
62636
+ signal?.throwIfAborted();
62637
+ execFile("git", [
62638
+ "ls-files",
62639
+ "-z",
62640
+ "--stage",
62641
+ "--others",
62642
+ "--exclude-standard"
62643
+ ], {
62644
+ cwd: rootDirectory,
62645
+ encoding: "utf-8",
62646
+ killSignal: "SIGKILL",
62647
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
62648
+ signal
62649
+ }, (error, stdout) => {
62650
+ if (signal?.aborted) {
62651
+ reject(signal.reason);
62652
+ return;
62653
+ }
62654
+ resolve(error ? null : parseGitSourceFilePaths(stdout));
62655
+ });
62656
+ });
62657
+ if (paths === null) return null;
62658
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
62659
+ const linguistIgnoredPaths = await collectGitLinguistIgnoredPathsCooperative(rootDirectory, candidatePaths, signal);
62660
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
62661
+ };
62461
62662
  const listSourceFilesViaFilesystem = (rootDirectory) => {
62462
62663
  const filePaths = [];
62463
62664
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
62464
62665
  return filePaths;
62465
62666
  };
62466
- const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
62667
+ const listSourceFilesViaFilesystemCooperative = async (rootDirectory, signal) => {
62668
+ const filePaths = [];
62669
+ let sliceStartedAt = performance.now();
62670
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
62671
+ signal?.throwIfAborted();
62672
+ if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
62673
+ if (performance.now() - sliceStartedAt >= 12) {
62674
+ await yieldToEventLoop();
62675
+ sliceStartedAt = performance.now();
62676
+ }
62677
+ }
62678
+ return filePaths;
62679
+ };
62680
+ const listSourceFilePaths = (rootDirectory) => (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort();
62681
+ const listSourceFilePathsCooperative = async (rootDirectory, signal) => (await listSourceFilesViaGitCooperative(rootDirectory, signal) ?? await listSourceFilesViaFilesystemCooperative(rootDirectory, signal)).sort();
62682
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilePaths(rootDirectory));
62683
+ const listSourceFilesCooperative = async (rootDirectory, signal) => (await collectSizedSourceFilesCooperative(rootDirectory, await listSourceFilePathsCooperative(rootDirectory, signal), signal)).map((entry) => entry.path);
62467
62684
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
62468
62685
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
62469
62686
  const UTF8_BOM_CHARACTER = "";
@@ -64041,13 +64258,7 @@ const hasReactDependency = (packageJson) => {
64041
64258
  };
64042
64259
  return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
64043
64260
  };
64044
- const getPreactVersion = (packageJson) => {
64045
- return {
64046
- ...packageJson.peerDependencies,
64047
- ...packageJson.dependencies,
64048
- ...packageJson.devDependencies
64049
- }.preact ?? null;
64050
- };
64261
+ const getPreactVersion = (packageJson) => getDependencySpec(packageJson, "preact");
64051
64262
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, sourceDirectory = rootDirectory, sourcePackageJson = rootPackageJson, version }) => {
64052
64263
  if (version === null || !isCatalogReference(version)) return version;
64053
64264
  const catalogName = extractCatalogName(version);
@@ -64792,7 +65003,16 @@ const getBrowserslistProtectionRequirement = (directory) => {
64792
65003
  if (!queries) return void 0;
64793
65004
  let requirement;
64794
65005
  let hasUnsupportedTarget = false;
64795
- for (const browserTarget of (0, import_browserslist.default)(queries)) {
65006
+ const previousIgnoreOldData = process.env.BROWSERSLIST_IGNORE_OLD_DATA;
65007
+ let browserTargets = [];
65008
+ try {
65009
+ process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true";
65010
+ browserTargets = (0, import_browserslist.default)(queries);
65011
+ } finally {
65012
+ if (previousIgnoreOldData === void 0) delete process.env.BROWSERSLIST_IGNORE_OLD_DATA;
65013
+ else process.env.BROWSERSLIST_IGNORE_OLD_DATA = previousIgnoreOldData;
65014
+ }
65015
+ for (const browserTarget of browserTargets) {
64796
65016
  const browserRequirement = getBrowserProtectionRequirement(browserTarget);
64797
65017
  if (browserRequirement === "unsupported") hasUnsupportedTarget = true;
64798
65018
  if (browserRequirement === "noreferrer") requirement = "noreferrer";
@@ -64902,9 +65122,9 @@ const clearProjectCache = () => {
64902
65122
  * framework-agnostic rules. Throws only when the directory has nothing
64903
65123
  * to scan (no enclosing project and no source files of its own).
64904
65124
  */
64905
- const discoverProjectWithoutPackageJson = (directory) => {
64906
- const sourceFileCount = countSourceFiles(directory);
64907
- const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
65125
+ const discoverProjectWithoutPackageJson = (directory, options) => {
65126
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
65127
+ const hasOwnTsConfig = isFile(Path.join(directory, "tsconfig.json"));
64908
65128
  const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
64909
65129
  const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
64910
65130
  if (enclosingProject !== null) return {
@@ -64970,12 +65190,15 @@ const discoverProjectWithoutPackageJson = (directory) => {
64970
65190
  sourceFileCount
64971
65191
  };
64972
65192
  };
64973
- const discoverProject = (directory) => {
65193
+ const discoverProject = (directory, options = {}) => {
64974
65194
  const cached = cachedProjectInfos.get(directory);
64975
- if (cached !== void 0) return cached;
65195
+ if (cached !== void 0) return options.sourceFileCount === void 0 ? cached : {
65196
+ ...cached,
65197
+ sourceFileCount: options.sourceFileCount
65198
+ };
64976
65199
  const packageJsonPath = Path.join(directory, "package.json");
64977
65200
  if (!isFile(packageJsonPath)) {
64978
- const synthesized = discoverProjectWithoutPackageJson(directory);
65201
+ const synthesized = discoverProjectWithoutPackageJson(directory, options);
64979
65202
  cachedProjectInfos.set(directory, synthesized);
64980
65203
  return synthesized;
64981
65204
  }
@@ -65037,8 +65260,8 @@ const discoverProject = (directory) => {
65037
65260
  const tailwindVersion = tailwindcss.version;
65038
65261
  const zodVersion = zod.version;
65039
65262
  const projectName = packageJson.name ?? Path.basename(directory);
65040
- const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
65041
- const sourceFileCount = countSourceFiles(directory);
65263
+ const hasTypeScript = isFile(Path.join(directory, "tsconfig.json"));
65264
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
65042
65265
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
65043
65266
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
65044
65267
  rootDirectory: directory,
@@ -65167,134 +65390,6 @@ const discoverProject = (directory) => {
65167
65390
  * single source file has nothing to analyze.
65168
65391
  */
65169
65392
  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
65393
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
65299
65394
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
65300
65395
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -65433,6 +65528,139 @@ const getEquivalentRuleKeys = (ruleKey) => {
65433
65528
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
65434
65529
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
65435
65530
  };
65531
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
65532
+ var InvalidGlobPatternError = class extends Error {
65533
+ pattern;
65534
+ reason;
65535
+ constructor(pattern, reason) {
65536
+ super(`Invalid glob pattern ${JSON.stringify(pattern)}: ${reason}`);
65537
+ this.name = "InvalidGlobPatternError";
65538
+ this.pattern = pattern;
65539
+ this.reason = reason;
65540
+ }
65541
+ };
65542
+ const assertGlobPattern = (condition, pattern, reason) => {
65543
+ if (!condition) throw new InvalidGlobPatternError(pattern, reason);
65544
+ };
65545
+ const countGlobWildcards = (pattern) => (pattern.match(/[*?]/g) ?? []).length;
65546
+ const normalizeGlobPattern = (pattern) => pattern.replace(/\\/g, "/").replace(/^\//, "");
65547
+ const PICOMATCH_OPTIONS = {
65548
+ dot: true,
65549
+ strictSlashes: false,
65550
+ windows: false
65551
+ };
65552
+ const compileGlobPattern = (rawPattern) => {
65553
+ assertGlobPattern(typeof rawPattern === "string" && rawPattern.length > 0, String(rawPattern), "pattern must be a non-empty string.");
65554
+ assertGlobPattern(rawPattern.length <= MAX_GLOB_PATTERN_LENGTH_CHARS, rawPattern, `pattern length ${rawPattern.length} exceeds the maximum of ${MAX_GLOB_PATTERN_LENGTH_CHARS} characters.`);
65555
+ const wildcardCount = countGlobWildcards(rawPattern);
65556
+ 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.`);
65557
+ try {
65558
+ return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
65559
+ } catch (caughtError) {
65560
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
65561
+ }
65562
+ };
65563
+ const compileGlobPatternsLenient = (patterns, onInvalid) => {
65564
+ const compiled = [];
65565
+ for (const pattern of patterns) try {
65566
+ compiled.push(compileGlobPattern(pattern));
65567
+ } catch (caughtError) {
65568
+ if (!(caughtError instanceof InvalidGlobPatternError)) throw caughtError;
65569
+ onInvalid(caughtError);
65570
+ }
65571
+ return compiled;
65572
+ };
65573
+ const toRelativePath = (filePath, rootDirectory) => {
65574
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
65575
+ const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
65576
+ if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
65577
+ return normalizedFilePath.replace(/^\.\//, "");
65578
+ };
65579
+ const warnConfigIssue = (message) => {
65580
+ process.stderr.write(`[react-doctor] ${message}\n`);
65581
+ };
65582
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
65583
+ const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65584
+ const hasMatchingRuleOverride = (ruleIds, ruleIdentifier) => {
65585
+ if (ruleIds.size === 0) return true;
65586
+ for (const ruleId of ruleIds) if (isSameRuleKey(ruleId, ruleIdentifier)) return true;
65587
+ return false;
65588
+ };
65589
+ const validateOverrideEntry = (entry, index) => {
65590
+ if (!isPlainObject(entry)) {
65591
+ warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
65592
+ return null;
65593
+ }
65594
+ if (!isStringArray(entry.files)) {
65595
+ warnConfigIssue(`ignore.overrides[${index}].files must be an array of strings; ignoring this entry.`);
65596
+ return null;
65597
+ }
65598
+ if (entry.rules !== void 0 && !isStringArray(entry.rules)) {
65599
+ 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).`);
65600
+ return { files: entry.files };
65601
+ }
65602
+ return entry.rules === void 0 ? { files: entry.files } : {
65603
+ files: entry.files,
65604
+ rules: entry.rules
65605
+ };
65606
+ };
65607
+ const compileIgnoreOverrides = (userConfig) => {
65608
+ const overrides = userConfig?.ignore?.overrides;
65609
+ if (overrides === void 0) return [];
65610
+ if (!Array.isArray(overrides)) {
65611
+ warnConfigIssue(`ignore.overrides must be an array of { files, rules } entries; ignoring.`);
65612
+ return [];
65613
+ }
65614
+ return overrides.flatMap((entry, index) => {
65615
+ const validated = validateOverrideEntry(entry, index);
65616
+ if (!validated) return [];
65617
+ const filePatterns = compileGlobPatternsLenient(collectStringList(validated.files), (error) => warnConfigIssue(`ignore.overrides[${index}]: ${error.message}`));
65618
+ if (filePatterns.length === 0) return [];
65619
+ return [{
65620
+ filePatterns,
65621
+ ruleIds: new Set(collectStringList(validated.rules))
65622
+ }];
65623
+ });
65624
+ };
65625
+ const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) => {
65626
+ if (overrides.length === 0) return false;
65627
+ const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
65628
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
65629
+ return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && hasMatchingRuleOverride(override.ruleIds, ruleIdentifier));
65630
+ };
65631
+ const SEVERITY_FOR_OVERRIDE = {
65632
+ error: "error",
65633
+ warn: "warning"
65634
+ };
65635
+ const restampSeverity = (diagnostic, override) => {
65636
+ const targetSeverity = SEVERITY_FOR_OVERRIDE[override];
65637
+ if (diagnostic.severity === targetSeverity) return diagnostic;
65638
+ return {
65639
+ ...diagnostic,
65640
+ severity: targetSeverity
65641
+ };
65642
+ };
65643
+ /**
65644
+ * Assembles the internal `RuleSeverityControls` shape from a user
65645
+ * config's top-level `rules` and `categories` fields — the
65646
+ * ESLint / oxlint-shaped severity surface.
65647
+ *
65648
+ * Returns `undefined` when neither field is present so the common
65649
+ * path (no severity config at all) stays allocation-free for
65650
+ * downstream consumers.
65651
+ */
65652
+ const buildRuleSeverityControls = (config) => {
65653
+ if (!config) return void 0;
65654
+ if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
65655
+ return {
65656
+ ...config.rules !== void 0 ? { rules: config.rules } : {},
65657
+ ...config.categories !== void 0 ? { categories: config.categories } : {},
65658
+ ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
65659
+ };
65660
+ };
65661
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
65662
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
65663
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
65436
65664
  const stripDescriptionTail = (ruleList) => {
65437
65665
  const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
65438
65666
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
@@ -65845,6 +66073,10 @@ const classifyByDirectoryCohort = (packageDirectory) => {
65845
66073
  current = parent;
65846
66074
  }
65847
66075
  };
66076
+ const clearPackageRoleCache = () => {
66077
+ cachedRoleByPackageDirectory.clear();
66078
+ cachedPackageDirectoryByFilename.clear();
66079
+ };
65848
66080
  const classifyPackageRole = (filename) => {
65849
66081
  if (!filename) return "unknown";
65850
66082
  const packageDirectory = findNearestPackageDirectory(filename);
@@ -66079,12 +66311,8 @@ const buildDiagnosticPipeline = (input) => {
66079
66311
  if (rule.tags.includes("migration-hint")) return false;
66080
66312
  return getFileContext(diagnostic.filePath) !== "production";
66081
66313
  };
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;
66314
+ const matchesRuleKey = (ruleIdentifier, candidates) => {
66315
+ for (const candidate of candidates) if (isSameRuleKey(candidate, ruleIdentifier)) return true;
66088
66316
  return false;
66089
66317
  };
66090
66318
  const isRnRawTextSuppressedByConfig = (diagnostic) => {
@@ -66126,12 +66354,13 @@ const buildDiagnosticPipeline = (input) => {
66126
66354
  if (explicitSeverityOverride === "off") return suppress(current, "config");
66127
66355
  if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
66128
66356
  }
66357
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
66129
66358
  if (explicitRuleOverride === void 0) {
66130
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
66359
+ if (matchesRuleKey(ruleIdentifier, APP_ONLY_RULE_KEYS) && isLibraryFile(current.filePath)) return null;
66131
66360
  }
66132
66361
  if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
66133
66362
  if (userConfig) {
66134
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
66363
+ if (matchesRuleKey(ruleIdentifier, ignoredRules)) return suppress(current, "config");
66135
66364
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
66136
66365
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
66137
66366
  if (isRnRawTextSuppressedByConfig(current)) return null;
@@ -66140,7 +66369,6 @@ const buildDiagnosticPipeline = (input) => {
66140
66369
  if (respectInlineDisables && current.line > 0) {
66141
66370
  const lines = getFileLines(current.filePath);
66142
66371
  if (lines) {
66143
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
66144
66372
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
66145
66373
  if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
66146
66374
  if (evaluation.nearMissHint) current = {
@@ -66394,7 +66622,8 @@ const readSystemFacts$1 = () => ({
66394
66622
  /**
66395
66623
  * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
66396
66624
  * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
66397
- * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
66625
+ * memory, and the measured automatic parallel-efficiency ceiling, then clamped
66626
+ * to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
66398
66627
  *
66399
66628
  * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
66400
66629
  * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
@@ -66410,7 +66639,7 @@ const readSystemFacts$1 = () => ({
66410
66639
  const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
66411
66640
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
66412
66641
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
66413
- return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
66642
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers, 10));
66414
66643
  };
66415
66644
  const resolveLintBatchOrdering = () => {
66416
66645
  return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
@@ -66423,24 +66652,22 @@ const resolveLintBatchOrdering = () => {
66423
66652
  */
66424
66653
  var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
66425
66654
  /**
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.
66655
+ * Optional Effect-side cap on the lint phase. Unbounded unless explicitly
66656
+ * configured through the env var or a test layer.
66428
66657
  * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
66429
66658
  */
66430
- var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
66659
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", null) }) {};
66431
66660
  /**
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
66661
+ * Optional Effect-side cap on the dead-code phase. Unbounded unless explicitly
66662
+ * configured through the env var or a test layer. Tests override via
66435
66663
  * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
66436
66664
  */
66437
- var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
66665
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", null) }) {};
66438
66666
  /**
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, ...)`.
66667
+ * Optional overall scan deadline. Unbounded unless explicitly configured
66668
+ * through the env var or a test layer.
66442
66669
  */
66443
- var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
66670
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", null) }) {};
66444
66671
  /**
66445
66672
  * Wall-clock budget for the supply-chain check when it runs on a background
66446
66673
  * fiber overlapping the lint pass. Reads from the env var on startup so the
@@ -66513,14 +66740,14 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
66513
66740
  /**
66514
66741
  * How the full-scan lint pass plans its file batches. `"cost"` (the default)
66515
66742
  * 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
66743
+ * batch count as greedy chunking, but every batch gets an even share of files
66744
+ * AND bytes, so no full chunk is a straggler while
66518
66745
  * the remainder-batch worker idles — and the heavy files are SPREAD across
66519
66746
  * batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
66520
66747
  * lacked (it packed the heaviest files into one wave-1 straggler batch,
66521
66748
  * measurably regressing size-skewed repos, which is why it never earned the
66522
66749
  * default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
66523
- * rollback hatch to plain greedy 100-file chunking in discovery order. Tests
66750
+ * rollback hatch to plain greedy fixed-size chunking in discovery order. Tests
66524
66751
  * override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
66525
66752
  * never reach this — they pass user-scoped `includePaths` that skip discovery
66526
66753
  * and stay in arrival order; only the full-scan branch reads it.
@@ -66615,6 +66842,7 @@ const DEFAULT_SURFACE_EXCLUDED_TAGS = {
66615
66842
  score: ["design"],
66616
66843
  ciFailure: ["design"]
66617
66844
  };
66845
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66618
66846
  const VALID_RULE_SEVERITIES = [
66619
66847
  "error",
66620
66848
  "warn",
@@ -66657,7 +66885,6 @@ const SURFACE_CONTROL_FIELD_NAMES = [
66657
66885
  ];
66658
66886
  const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
66659
66887
  const SEVERITY_FIELD_NAMES = ["rules", "categories"];
66660
- const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66661
66888
  const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
66662
66889
  const isRuleSeverity = (value) => typeof value === "string" && VALID_RULE_SEVERITIES.includes(value);
66663
66890
  const coerceMaybeBooleanString = (fieldName, value) => {
@@ -66685,7 +66912,7 @@ const validateStringArrayField = (fieldName, value) => {
66685
66912
  });
66686
66913
  };
66687
66914
  const validateSurfaceControls = (surface, rawControls) => {
66688
- if (!isPlainObject$1(rawControls)) {
66915
+ if (!isRecord(rawControls)) {
66689
66916
  warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
66690
66917
  return;
66691
66918
  }
@@ -66709,7 +66936,7 @@ const validateSurfaceControls = (surface, rawControls) => {
66709
66936
  return validatedSurfaceControls;
66710
66937
  };
66711
66938
  const validateSurfacesField = (rawSurfaces) => {
66712
- if (!isPlainObject$1(rawSurfaces)) {
66939
+ if (!isRecord(rawSurfaces)) {
66713
66940
  warnConfigIssue(`config field "surfaces" must be an object (got ${typeof rawSurfaces}); ignoring this field.`);
66714
66941
  return;
66715
66942
  }
@@ -66725,7 +66952,7 @@ const validateSurfacesField = (rawSurfaces) => {
66725
66952
  return validated;
66726
66953
  };
66727
66954
  const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
66728
- if (!isPlainObject$1(rawMap)) {
66955
+ if (!isRecord(rawMap)) {
66729
66956
  warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
66730
66957
  return;
66731
66958
  }
@@ -67010,6 +67237,10 @@ const dedupeRelatedDiagnostics = (diagnostics) => {
67010
67237
  }
67011
67238
  return uniqueDiagnostics;
67012
67239
  };
67240
+ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
67241
+ const relativePath = Path.relative(parentAbsolutePath, childAbsolutePath);
67242
+ return Boolean(relativePath) && !relativePath.startsWith("..") && !Path.isAbsolute(relativePath);
67243
+ };
67013
67244
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
67014
67245
  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
67246
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -68586,7 +68817,7 @@ const readScannedFile = (candidate) => {
68586
68817
  return null;
68587
68818
  }
68588
68819
  };
68589
- function* collectSecurityScanFiles(rootDirectory) {
68820
+ function* collectSecurityScanFiles(rootDirectory, excludedDirectories = /* @__PURE__ */ new Set()) {
68590
68821
  const priorityCandidates = [];
68591
68822
  const artifactCandidates = [];
68592
68823
  const otherCandidates = [];
@@ -68601,7 +68832,7 @@ function* collectSecurityScanFiles(rootDirectory) {
68601
68832
  for (const entry of readDirectoryEntries(current.absolutePath)) {
68602
68833
  const absolutePath = Path.join(current.absolutePath, entry.name);
68603
68834
  if (entry.isDirectory()) {
68604
- if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) stack.push({
68835
+ if (!SKIPPED_DIRECTORY_NAMES.has(entry.name) && !excludedDirectories.has(Path.resolve(absolutePath))) stack.push({
68605
68836
  absolutePath,
68606
68837
  depth: current.depth + 1
68607
68838
  });
@@ -68782,10 +69013,8 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhe
68782
69013
  }
68783
69014
  return true;
68784
69015
  };
69016
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
68785
69017
  const shouldEnableRuleByDefaultStatus = (options) => options.defaultEnabled !== false || options.hasExplicitOverride === true || options.includeTagDefaults && options.hasIncludedTags;
68786
- const yieldToEventLoop = () => new Promise((resolve) => {
68787
- setImmediate(resolve);
68788
- });
68789
69018
  const createSecurityScanSession = (rootDirectory, options) => {
68790
69019
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
68791
69020
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
@@ -68838,19 +69067,35 @@ const createSecurityScanSession = (rootDirectory, options) => {
68838
69067
  };
68839
69068
  };
68840
69069
  const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
69070
+ const throwIfScanAborted = () => {
69071
+ options.signal?.throwIfAborted();
69072
+ };
69073
+ const didReachDeadline = () => {
69074
+ const hasDeadlineElapsed = options.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(options.deadlineEpochMs) === 0;
69075
+ if (hasDeadlineElapsed) options.onDeadlineExceeded?.();
69076
+ return hasDeadlineElapsed;
69077
+ };
69078
+ throwIfScanAborted();
68841
69079
  const session = createSecurityScanSession(rootDirectory, options);
68842
69080
  if (session === null) return [];
69081
+ if (didReachDeadline()) return [];
68843
69082
  let sliceStartedAt = performance.now();
68844
- for (const file of collectSecurityScanFiles(rootDirectory)) {
69083
+ for (const file of collectSecurityScanFiles(rootDirectory, options.excludedDirectories)) {
69084
+ throwIfScanAborted();
69085
+ if (didReachDeadline()) return session.diagnostics;
68845
69086
  if (file === null) {
68846
69087
  if (performance.now() - sliceStartedAt >= 12) {
68847
69088
  await yieldToEventLoop();
69089
+ throwIfScanAborted();
69090
+ if (didReachDeadline()) return session.diagnostics;
68848
69091
  sliceStartedAt = performance.now();
68849
69092
  }
68850
69093
  continue;
68851
69094
  }
68852
69095
  for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
68853
69096
  await yieldToEventLoop();
69097
+ throwIfScanAborted();
69098
+ if (didReachDeadline()) return session.diagnostics;
68854
69099
  sliceStartedAt = performance.now();
68855
69100
  }
68856
69101
  }
@@ -69013,25 +69258,18 @@ const collectIgnorePatterns = (rootDirectory) => {
69013
69258
  cachedPatternsByRoot.set(rootDirectory, patterns);
69014
69259
  return patterns;
69015
69260
  };
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
- }
69261
+ const failOpenReadJson = (filePath, fallback) => {
69025
69262
  try {
69026
- return JSON.parse(rawContents);
69263
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
69027
69264
  } catch {
69028
- return null;
69265
+ return fallback;
69029
69266
  }
69030
69267
  };
69268
+ const KNIP_JSON_FILENAME = "knip.json";
69031
69269
  const readKnipConfig = (rootDirectory) => {
69032
- const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
69270
+ const knipJson = failOpenReadJson(path.join(rootDirectory, KNIP_JSON_FILENAME), null);
69033
69271
  if (isRecord(knipJson)) return knipJson;
69034
- const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
69272
+ const packageJson = failOpenReadJson(path.join(rootDirectory, "package.json"), null);
69035
69273
  const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
69036
69274
  return isRecord(packageKnipConfig) ? packageKnipConfig : null;
69037
69275
  };
@@ -69079,13 +69317,6 @@ const atomicWriteJson = (filePath, value) => {
69079
69317
  return;
69080
69318
  }
69081
69319
  };
69082
- const failOpenReadJson = (filePath, fallback) => {
69083
- try {
69084
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
69085
- } catch {
69086
- return fallback;
69087
- }
69088
- };
69089
69320
  const hashFileContents = (filePath) => {
69090
69321
  try {
69091
69322
  return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
@@ -69764,7 +69995,6 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
69764
69995
  return true;
69765
69996
  };
69766
69997
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
69767
- const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
69768
69998
  /**
69769
69999
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
69770
70000
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -69772,25 +70002,28 @@ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs
69772
70002
  * tipped over by any concurrent load — silently dropping every dead-code
69773
70003
  * finding. Scaling the budget with file count (and inversely with the core
69774
70004
  * 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.
70005
+ * reclaims a genuinely wedged worker.
69777
70006
  */
69778
70007
  const resolveDeadCodeTimeout = (input) => {
69779
70008
  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
- };
70009
+ return { workerTimeoutMs: Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor))) };
69785
70010
  };
69786
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
70011
+ const resolveLintIncludePaths = (rootDirectory, userConfig, sourceFiles) => {
69787
70012
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
69788
70013
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
69789
- return listSourceFiles(rootDirectory).filter((filePath) => {
70014
+ return (sourceFiles ?? listSourceFiles(rootDirectory)).filter((filePath) => {
69790
70015
  if (!isLintableSourceFile(filePath)) return false;
69791
70016
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
69792
70017
  });
69793
70018
  };
70019
+ const filterPathsOutsideDirectories = (input) => {
70020
+ 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}/`)));
70021
+ if (excludedPrefixes.length === 0) return [...input.relativePaths];
70022
+ return input.relativePaths.filter((relativePath) => {
70023
+ const normalizedRelativePath = relativePath.replaceAll("\\", "/").replace(/^\.\//, "");
70024
+ return !excludedPrefixes.some((excludedPrefix) => normalizedRelativePath === excludedPrefix || normalizedRelativePath.startsWith(`${excludedPrefix}/`));
70025
+ });
70026
+ };
69794
70027
  var Config = class Config extends Service()("react-doctor/Config") {
69795
70028
  static layerNode = effect(Config, gen(function* () {
69796
70029
  const cache = yield* make$3({
@@ -69852,6 +70085,7 @@ var Files = class Files extends Service()("react-doctor/Files") {
69852
70085
  static layerNode = succeed$3(Files, Files.of({
69853
70086
  readLines: (input) => sync(() => createNodeReadFileLinesSync(input.rootDirectory)(input.filePath)),
69854
70087
  listSourceFiles: (rootDirectory) => sync(() => listSourceFiles(rootDirectory)),
70088
+ listSourceFilesCooperative: (input) => promise(() => listSourceFilesCooperative(input.rootDirectory, input.signal)),
69855
70089
  isFile: (filePath) => sync(() => isFile(filePath)),
69856
70090
  isDirectory: (filePath) => sync(() => isDirectory(filePath))
69857
70091
  }));
@@ -69878,6 +70112,16 @@ var Files = class Files extends Service()("react-doctor/Files") {
69878
70112
  }
69879
70113
  return files;
69880
70114
  }),
70115
+ listSourceFilesCooperative: (input) => sync(() => {
70116
+ const rootDirectory = input.rootDirectory;
70117
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
70118
+ const files = [];
70119
+ for (const absolute of tree.keys()) {
70120
+ if (!absolute.startsWith(prefix)) continue;
70121
+ files.push(absolute.slice(prefix.length));
70122
+ }
70123
+ return files;
70124
+ }),
69881
70125
  isFile: (filePath) => sync(() => tree.has(filePath)),
69882
70126
  isDirectory: (filePath) => sync(() => {
69883
70127
  const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
@@ -70453,7 +70697,7 @@ const batchIncludePaths = (baseArgs, includePaths) => {
70453
70697
  for (const filePath of includePaths) {
70454
70698
  const entryLength = filePath.length + 1;
70455
70699
  const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
70456
- const exceedsFileCount = currentBatch.length >= 100;
70700
+ const exceedsFileCount = currentBatch.length >= 200;
70457
70701
  if (exceedsArgLength || exceedsFileCount) {
70458
70702
  batches.push(currentBatch);
70459
70703
  currentBatch = [];
@@ -70800,7 +71044,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
70800
71044
  }
70801
71045
  return enabled;
70802
71046
  };
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 }) => {
71047
+ 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
71048
  const hasIncludedTags = includedTags.size > 0;
70805
71049
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" || hasIncludedTags ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
70806
71050
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
@@ -70864,7 +71108,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
70864
71108
  }))
70865
71109
  } : {},
70866
71110
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
70867
- ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
71111
+ ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {},
71112
+ ...projectIndexModuleSources !== void 0 ? { projectIndexModuleSources: [...projectIndexModuleSources] } : {}
70868
71113
  } },
70869
71114
  rules: {
70870
71115
  ...reactCompilerRules,
@@ -71113,11 +71358,17 @@ const collectUnpluginAutoImportGlobalScopes = ({ rootDirectory, candidateFiles }
71113
71358
  return scopes.some((scope) => scope.names.length > 0) ? scopes : [];
71114
71359
  };
71115
71360
  const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
71361
+ const decodeDiagnostic = (raw) => {
71362
+ const diagnostic = validateDiagnostic$1(raw);
71363
+ return {
71364
+ ...diagnostic,
71365
+ relatedLocations: diagnostic.relatedLocations?.map((location) => ({ ...location }))
71366
+ };
71367
+ };
71116
71368
  const decodeFileDiagnostics = (raw) => {
71117
71369
  if (!Array.isArray(raw)) return null;
71118
71370
  try {
71119
- for (const entry of raw) validateDiagnostic$1(entry);
71120
- return raw;
71371
+ return raw.map((entry) => decodeDiagnostic(entry));
71121
71372
  } catch {
71122
71373
  return null;
71123
71374
  }
@@ -72349,6 +72600,12 @@ const buildProfiledNodeArguments = (input) => [
72349
72600
  ...input.heapProfileDirectory ? ["--heap-prof", `--heap-prof-dir=${input.heapProfileDirectory}`] : [],
72350
72601
  ...input.argumentsList
72351
72602
  ];
72603
+ const lowerChildProcessPriority = (processId) => {
72604
+ if (processId === void 0) return;
72605
+ try {
72606
+ OS.setPriority(processId, OS.constants.priority.PRIORITY_BELOW_NORMAL);
72607
+ } catch {}
72608
+ };
72352
72609
  const SANITIZED_ENV = buildOxlintChildEnv(process.env);
72353
72610
  /**
72354
72611
  * Spawn one oxlint subprocess with hard ceilings on wall time and
@@ -72387,6 +72644,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
72387
72644
  "pipe"
72388
72645
  ]
72389
72646
  });
72647
+ lowerChildProcessPriority(child.pid);
72390
72648
  const onAbort = () => {
72391
72649
  child.kill("SIGKILL");
72392
72650
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
@@ -72509,7 +72767,7 @@ const isParallelismRelatedSpawnError = (error) => {
72509
72767
  * loop with a slimmer config in that case.
72510
72768
  */
72511
72769
  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;
72770
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 9, signal, deadlineEpochMs } = input;
72513
72771
  const resolveSourcePath = (filePath) => {
72514
72772
  const absoluteFilePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory, filePath);
72515
72773
  return sourcePathByLintPath?.get(Path.normalize(absoluteFilePath)) ?? filePath;
@@ -72517,6 +72775,12 @@ const spawnLintBatches = async (input) => {
72517
72775
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
72518
72776
  let rescueDeadlineEpochMs;
72519
72777
  const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
72778
+ const resolveSpawnTimeoutMs = () => {
72779
+ const deadlineBudgets = [deadlineEpochMs, rescueDeadlineEpochMs].filter((deadline) => deadline !== void 0).map(remainingDeadlineBudgetMs);
72780
+ if (deadlineBudgets.length === 0) return spawnTimeoutMs;
72781
+ const remainingDeadlineMs = Math.min(...deadlineBudgets);
72782
+ return spawnTimeoutMs === void 0 ? remainingDeadlineMs : Math.min(spawnTimeoutMs, remainingDeadlineMs);
72783
+ };
72520
72784
  const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
72521
72785
  const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
72522
72786
  const allDiagnostics = [];
@@ -72534,12 +72798,13 @@ const spawnLintBatches = async (input) => {
72534
72798
  const batchArgs = [...baseArgs, ...batch];
72535
72799
  try {
72536
72800
  const spawnBatch = () => {
72537
- if (isPastDeadline()) {
72801
+ const effectiveSpawnTimeoutMs = resolveSpawnTimeoutMs();
72802
+ if (effectiveSpawnTimeoutMs === 0) {
72538
72803
  deadlineSkippedFiles.push(...batch);
72539
72804
  batchState.deadlineSkippedFileCount += batch.length;
72540
72805
  return Promise.resolve(null);
72541
72806
  }
72542
- return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
72807
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, effectiveSpawnTimeoutMs, outputMaxBytes, signal, () => {
72543
72808
  if (batchState.didStart) return;
72544
72809
  batchState.didStart = true;
72545
72810
  startedFileCount += batchState.initialFileCount;
@@ -72694,11 +72959,36 @@ const validateRuleRegistration = () => {
72694
72959
  ].filter((entry) => entry !== null).join("; ");
72695
72960
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
72696
72961
  };
72962
+ const PROJECT_INDEX_MODULE_SOURCES = [
72963
+ "next/og",
72964
+ "@vercel/og",
72965
+ "satori",
72966
+ "remotion"
72967
+ ];
72968
+ const collectProjectIndexModuleSources = async (rootDirectory, candidateFiles) => {
72969
+ const foundModuleSources = /* @__PURE__ */ new Set();
72970
+ let sliceStartedAt = performance.now();
72971
+ for (const candidateFile of candidateFiles) {
72972
+ let sourceBuffer;
72973
+ try {
72974
+ sourceBuffer = NFS.readFileSync(Path.isAbsolute(candidateFile) ? candidateFile : Path.resolve(rootDirectory, candidateFile));
72975
+ } catch {
72976
+ continue;
72977
+ }
72978
+ for (const moduleSource of PROJECT_INDEX_MODULE_SOURCES) if (sourceBuffer.includes(`"${moduleSource}"`) || sourceBuffer.includes(`'${moduleSource}'`)) foundModuleSources.add(moduleSource);
72979
+ if (foundModuleSources.size === PROJECT_INDEX_MODULE_SOURCES.length) break;
72980
+ if (performance.now() - sliceStartedAt >= 12) {
72981
+ await yieldToEventLoop();
72982
+ sliceStartedAt = performance.now();
72983
+ }
72984
+ }
72985
+ return [...foundModuleSources].sort();
72986
+ };
72697
72987
  /**
72698
72988
  * 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
72989
+ * `batchIncludePaths` greedily fills fixed-size batches and leaves a small
72990
+ * remainder batch, so the parallel pool's wall clock is set by whichever
72991
+ * full chunk happened to collect the most work while
72702
72992
  * the remainder's worker idles), this planner keeps the SAME mandatory batch
72703
72993
  * count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
72704
72994
  * perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
@@ -72727,7 +73017,7 @@ const validateRuleRegistration = () => {
72727
73017
  */
72728
73018
  const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
72729
73019
  if (files.length === 0) return [];
72730
- const batchCount = Math.ceil(files.length / 100);
73020
+ const batchCount = Math.ceil(files.length / 200);
72731
73021
  const maxFilesPerBatch = Math.ceil(files.length / batchCount);
72732
73022
  const baseArgsLengthChars = estimateArgsLength(baseArgs);
72733
73023
  const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
@@ -73256,6 +73546,7 @@ const runOxlint = async (options) => {
73256
73546
  await yieldToEventLoop();
73257
73547
  const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
73258
73548
  const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
73549
+ const projectIndexModuleSources = includePaths === void 0 && options.deadlineEpochMs === void 0 ? await collectProjectIndexModuleSources(rootDirectory, candidateFiles) : void 0;
73259
73550
  unpluginAutoImportGlobalScopes = collectUnpluginAutoImportGlobalScopes({
73260
73551
  rootDirectory,
73261
73552
  candidateFiles
@@ -73278,6 +73569,7 @@ const runOxlint = async (options) => {
73278
73569
  runtimeGlobals,
73279
73570
  unpluginAutoImportGlobalScopes,
73280
73571
  serverAuthFunctionNames,
73572
+ projectIndexModuleSources,
73281
73573
  severityControls,
73282
73574
  userPlugins,
73283
73575
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
@@ -73745,10 +74037,10 @@ const translateProjectInfoError = (cause, directory) => {
73745
74037
  }) });
73746
74038
  };
73747
74039
  var Project = class Project extends Service()("react-doctor/Project") {
73748
- static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (directory) {
74040
+ static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (input) {
73749
74041
  return yield* try_({
73750
- try: () => discoverProject(directory),
73751
- catch: (cause) => translateProjectInfoError(cause, directory)
74042
+ try: () => discoverProject(input.directory, { sourceFileCount: input.sourceFileCount }),
74043
+ catch: (cause) => translateProjectInfoError(cause, input.directory)
73752
74044
  });
73753
74045
  }) }));
73754
74046
  static layerOf = (projectInfo) => succeed$3(Project, Project.of({ discover: () => succeed$2(projectInfo) }));
@@ -74333,9 +74625,10 @@ const checkSupplyChain = (input) => gen(function* () {
74333
74625
  * outage.
74334
74626
  */
74335
74627
  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
74628
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (configuredTimeoutMs) => checkSupplyChain({
74629
+ rootDirectory: input.rootDirectory,
74630
+ userConfig: input.userConfig,
74631
+ totalTimeoutMs: input.timeoutMs ?? configuredTimeoutMs
74339
74632
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
74340
74633
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
74341
74634
  };
@@ -74370,10 +74663,6 @@ const NO_HOOKS = {
74370
74663
  beforeLint: () => void_,
74371
74664
  afterLint: () => void_
74372
74665
  };
74373
- const filterMapNullable = (transform) => fromPredicateOption((value) => {
74374
- const result = transform(value);
74375
- return result === null ? none() : some(result);
74376
- });
74377
74666
  const fileReader = (filesService, rootDirectory) => (filePath) => {
74378
74667
  const lines = runSync(filesService.readLines({
74379
74668
  filePath,
@@ -74397,7 +74686,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
74397
74686
  * The GitHub viewer-permission lookup is forked onto a background
74398
74687
  * fiber here and joined late (it feeds score metadata, not
74399
74688
  * diagnostics).
74400
- * 2. beforeLint hook (e.g. CLI renders the project-detection block)
74689
+ * 2. beforeLint hook (e.g. CLI records project telemetry)
74401
74690
  * 3. environment checks (reduced-motion + pnpm hardening +
74402
74691
  * expo/react-native), collected synchronously. The heavier
74403
74692
  * content-regex security scan is forked instead (like supply-chain
@@ -74410,13 +74699,11 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
74410
74699
  * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
74411
74700
  * socket can't drag out its join; on timeout it fails open to no
74412
74701
  * 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
74702
+ * 5. Linter.run runs; DeadCode.run forks only when
74703
+ * REACT_DOCTOR_DEAD_CODE_OVERLAP=on. The default "auto" and explicit
74704
+ * "off" paths stay sequential. The fiber is joined (or interrupted,
74705
+ * SIGKILLing its worker, on lint failure) before diagnostics are
74706
+ * concatenated. The afterLint hook
74420
74707
  * fires between lint and dead-code. Progress spinner labels AND the
74421
74708
  * final diagnostic / score order stay independent of execution
74422
74709
  * order, so terminal output is identical either way; supply-chain
@@ -74451,8 +74738,28 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74451
74738
  const partialFailuresRef = yield* LintPartialFailures;
74452
74739
  const resolvedConfig = yield* configService.resolve(input.directory);
74453
74740
  const scanDirectory = resolvedConfig.resolvedDirectory;
74454
- const project = yield* projectService.discover(scanDirectory);
74741
+ const excludedProjectDirectories = (input.excludedProjectDirectories ?? []).map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, scanDirectory));
74742
+ const excludedProjectRelativePaths = excludedProjectDirectories.map((excludedDirectory) => Path.relative(scanDirectory, excludedDirectory).replaceAll("\\", "/"));
74743
+ const isExcludedProjectDiagnostic = (diagnostic) => {
74744
+ const relativeFilePath = toNormalizedRelativePath(diagnostic.filePath, scanDirectory);
74745
+ return excludedProjectRelativePaths.some((excludedRelativePath) => relativeFilePath === excludedRelativePath || relativeFilePath.startsWith(`${excludedRelativePath}/`));
74746
+ };
74747
+ const precomputedSourceFiles = input.suppressScanSummary === true || excludedProjectDirectories.length > 0 ? yield* filesService.listSourceFilesCooperative({
74748
+ rootDirectory: scanDirectory,
74749
+ signal: input.signal
74750
+ }) : null;
74751
+ const includedPrecomputedSourceFiles = precomputedSourceFiles !== null && excludedProjectDirectories.length > 0 ? filterPathsOutsideDirectories({
74752
+ rootDirectory: scanDirectory,
74753
+ relativePaths: precomputedSourceFiles,
74754
+ excludedDirectories: excludedProjectDirectories
74755
+ }) : precomputedSourceFiles;
74756
+ const sourceFileCount = excludedProjectDirectories.length > 0 ? includedPrecomputedSourceFiles?.length : input.precomputedSourceFileCount ?? includedPrecomputedSourceFiles?.length;
74757
+ const project = yield* projectService.discover({
74758
+ directory: scanDirectory,
74759
+ sourceFileCount
74760
+ });
74455
74761
  if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
74762
+ const deadCodeSourceFileCount = input.retainExcludedProjectDeadCodeDiagnostics === true && precomputedSourceFiles !== null ? precomputedSourceFiles.length : project.sourceFileCount;
74456
74763
  const [repo, sha, defaultBranch] = yield* all([
74457
74764
  gitService.githubRepo(scanDirectory).pipe(orElseSucceed(() => null)),
74458
74765
  gitService.headSha(scanDirectory).pipe(orElseSucceed(() => null)),
@@ -74463,11 +74770,16 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74463
74770
  directory: scanDirectory,
74464
74771
  repo
74465
74772
  }).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)) : [];
74773
+ let lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, includedPrecomputedSourceFiles ?? void 0);
74774
+ if (excludedProjectDirectories.length > 0) lintIncludePaths = filterPathsOutsideDirectories({
74775
+ rootDirectory: scanDirectory,
74776
+ relativePaths: lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory)),
74777
+ excludedDirectories: excludedProjectDirectories
74778
+ });
74779
+ const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
74468
74780
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
74469
74781
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
74470
- yield* beforeLint(project, lintIncludePaths ?? void 0);
74782
+ yield* beforeLint(project);
74471
74783
  const isDiffMode = input.includePaths.length > 0;
74472
74784
  const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? true;
74473
74785
  const transform = buildDiagnosticPipeline({
@@ -74477,7 +74789,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74477
74789
  respectInlineDisables: input.respectInlineDisables,
74478
74790
  showWarnings
74479
74791
  });
74480
- const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
74792
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(fromPredicateOption((diagnostic) => {
74793
+ const filteredDiagnostic = transform.apply(diagnostic);
74794
+ return filteredDiagnostic === null ? none() : some(filteredDiagnostic);
74795
+ })));
74481
74796
  const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
74482
74797
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
74483
74798
  ...checkReducedMotion(scanDirectory),
@@ -74487,17 +74802,31 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74487
74802
  ...checkReactNativeProject(scanDirectory, project)
74488
74803
  ])));
74489
74804
  const securityScanFailedRef = yield* make$13(false);
74490
- const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
74805
+ let didSecurityScanReachDeadline = false;
74806
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise((signal) => checkSecurityScanCooperative(scanDirectory, {
74491
74807
  project,
74492
74808
  ignoredTags: input.ignoredTags,
74493
74809
  includedTags: input.includedTags,
74494
- includeTagDefaults: input.includeTagDefaults
74810
+ includeTagDefaults: input.includeTagDefaults,
74811
+ excludedDirectories: new Set(excludedProjectDirectories),
74812
+ deadlineEpochMs: input.deadlineEpochMs,
74813
+ signal,
74814
+ onDeadlineExceeded: () => {
74815
+ didSecurityScanReachDeadline = true;
74816
+ }
74495
74817
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
74818
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
74819
+ const capOptionalToDeadline = (phaseTimeoutMs) => {
74820
+ if (input.deadlineEpochMs === void 0) return phaseTimeoutMs;
74821
+ const remainingBudgetMs = remainingDeadlineBudgetMs(input.deadlineEpochMs);
74822
+ return phaseTimeoutMs === null ? remainingBudgetMs : Math.min(phaseTimeoutMs, remainingBudgetMs);
74823
+ };
74496
74824
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
74497
- const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
74825
+ const supplyChainOverlapTimeout = capToDeadline(yield* SupplyChainOverlapTimeoutMs);
74498
74826
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
74499
74827
  rootDirectory: scanDirectory,
74500
- userConfig: resolvedConfig.config
74828
+ userConfig: resolvedConfig.config,
74829
+ timeoutMs: supplyChainOverlapTimeout
74501
74830
  }))).pipe(map$3((diagnostics) => ({
74502
74831
  diagnostics,
74503
74832
  timedOut: false
@@ -74521,46 +74850,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74521
74850
  const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
74522
74851
  const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
74523
74852
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
74524
- const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
74525
74853
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
74526
- const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
74527
74854
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
74528
74855
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
74529
74856
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
74530
74857
  const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
74531
74858
  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
- })));
74859
+ const buildCollectDeadCode = (deadCodeTimeout) => {
74860
+ const collectDeadCode = runCollect(applyPerElementPipeline(deadCodeService.run({
74861
+ rootDirectory: scanDirectory,
74862
+ parseConcurrency: deadCodeParseConcurrency,
74863
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
74864
+ onCacheOutcome: (didHitCache) => {
74865
+ deadCodeCacheHit = didHitCache;
74866
+ },
74867
+ onSummaryCacheStats: (stats) => {
74868
+ deadCodeSummaryCacheHits = stats.hits;
74869
+ deadCodeSummaryCacheMisses = stats.misses;
74870
+ }
74871
+ }).pipe(filter((diagnostic) => input.retainExcludedProjectDeadCodeDiagnostics === true || !isExcludedProjectDiagnostic(diagnostic)), catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
74872
+ yield* set(deadCodeFailure, {
74873
+ didFail: true,
74874
+ reason: error.message
74875
+ });
74876
+ return empty$4;
74877
+ }))))));
74878
+ const phaseTimeoutMs = deadCodeTimeout.phaseTimeoutMs;
74879
+ if (phaseTimeoutMs === null) return collectDeadCode;
74880
+ return collectDeadCode.pipe(timeoutOption(phaseTimeoutMs), flatMap$2(match$3({
74881
+ onNone: () => set(deadCodeFailure, {
74882
+ didFail: true,
74883
+ reason: `Dead-code analysis exceeded ${Math.round(phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
74884
+ }).pipe(as([])),
74885
+ onSome: succeed$2
74886
+ })));
74887
+ };
74556
74888
  const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
74557
- sourceFileCount: project.sourceFileCount,
74889
+ sourceFileCount: deadCodeSourceFileCount,
74558
74890
  deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
74559
74891
  fullConcurrency: scanConcurrency
74560
74892
  });
74561
74893
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
74562
74894
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
74563
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
74895
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
74564
74896
  })) : null;
74565
74897
  const scanProgress = yield* progressService.start("Scanning...");
74566
74898
  const scanStartTime = Date.now();
@@ -74611,7 +74943,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74611
74943
  });
74612
74944
  return empty$4;
74613
74945
  }))));
74614
- const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
74946
+ const collectLintDiagnostics = runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream));
74947
+ const lintCollected = dedupeRelatedDiagnostics(yield* lintPhaseTimeoutMs === null ? collectLintDiagnostics : collectLintDiagnostics.pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
74615
74948
  onNone: () => set(lintFailure, {
74616
74949
  didFail: true,
74617
74950
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -74641,13 +74974,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74641
74974
  } else {
74642
74975
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
74643
74976
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
74644
- sourceFileCount: totalFileCount,
74977
+ sourceFileCount: input.retainExcludedProjectDeadCodeDiagnostics === true ? deadCodeSourceFileCount : totalFileCount,
74645
74978
  deadCodeConcurrency: scanConcurrency,
74646
74979
  fullConcurrency: scanConcurrency
74647
74980
  });
74648
74981
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
74649
74982
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
74650
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
74983
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
74651
74984
  });
74652
74985
  }
74653
74986
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -74691,7 +75024,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74691
75024
  metadata: scoreMetadata
74692
75025
  });
74693
75026
  const lintPartialFailures = yield* get$2(partialFailuresRef);
74694
- const securityScanFailed = yield* get$2(securityScanFailedRef);
75027
+ const didSecurityScanFail = yield* get$2(securityScanFailedRef);
75028
+ const securityScanFailed = didSecurityScanFail || didSecurityScanReachDeadline;
75029
+ let securityScanFailureReason = null;
75030
+ if (didSecurityScanReachDeadline) securityScanFailureReason = "Security scan reached the max scan duration; findings collected before the deadline were preserved.";
75031
+ else if (didSecurityScanFail) securityScanFailureReason = "Security scan failed and was skipped.";
74695
75032
  return {
74696
75033
  project,
74697
75034
  userConfig: resolvedConfig.config,
@@ -74714,6 +75051,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74714
75051
  scanConcurrency,
74715
75052
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
74716
75053
  securityScanFailed,
75054
+ securityScanFailureReason,
74717
75055
  lintCacheHitFileCount,
74718
75056
  lintCacheTotalFileCount,
74719
75057
  lintSidecarReplayedFileCount,
@@ -74729,7 +75067,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74729
75067
  "inspect.runDeadCode": input.runDeadCode,
74730
75068
  "inspect.isCi": input.isCi,
74731
75069
  "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` }) })))));
75070
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => {
75071
+ if (scanDeadlineMs === null) return scanProgram;
75072
+ return scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })));
75073
+ }));
74733
75074
  const parseNodeVersion = (versionString) => {
74734
75075
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
74735
75076
  return {
@@ -74830,16 +75171,6 @@ const resolveNodeForOxlint = () => {
74830
75171
  });
74831
75172
  });
74832
75173
  /**
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
75174
  * Writes a set of source files (supplied by `readContent` — e.g.
74844
75175
  * `git show <ref>:<path>` for a baseline tree, or `git show :<path>` for the
74845
75176
  * index) into a temp tree that mirrors the project layout, copying the
@@ -75016,14 +75347,6 @@ const runEditorScan = async (input) => {
75016
75347
  error: messageFromUnknown(error)
75017
75348
  };
75018
75349
  };
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
75350
  const lookupOwnRule = (rule) => Object.hasOwn(REACT_DOCTOR_RULE_REGISTRY, rule) ? REACT_DOCTOR_RULE_REGISTRY[rule] : void 0;
75028
75351
  /**
75029
75352
  * Returns presentation metadata for `<plugin>/<rule>`, or `null` when the
@@ -75063,7 +75386,7 @@ const computeConfigFingerprint = (projectDirectory, version) => {
75063
75386
  /** Display name used in client-facing messages and progress titles. */
75064
75387
  const SERVER_DISPLAY_NAME = "React Doctor";
75065
75388
  /** Server version reported in `serverInfo`; injected at build, `dev` from source. */
75066
- const SERVER_VERSION = "0.9.3";
75389
+ const SERVER_VERSION = "0.9.4";
75067
75390
  /** `Diagnostic.source` shown next to every published diagnostic. */
75068
75391
  const DIAGNOSTIC_SOURCE = "react-doctor";
75069
75392
  /**
@@ -75691,10 +76014,7 @@ const createProjectGraph = (options) => {
75691
76014
  for (const root of roots) try {
75692
76015
  for (const workspacePackage of discoverReactSubprojects(root)) {
75693
76016
  const directory = normalizeDirectory(workspacePackage.directory);
75694
- if (!seen.has(directory)) seen.set(directory, {
75695
- directory,
75696
- name: workspacePackage.name
75697
- });
76017
+ if (!seen.has(directory)) seen.set(directory, { directory });
75698
76018
  }
75699
76019
  } catch (error) {
75700
76020
  logger.warn(`Project discovery failed for ${root}: ${messageFromUnknown(error)}`);
@@ -75719,8 +76039,8 @@ const createProjectGraph = (options) => {
75719
76039
  clearProjectCache();
75720
76040
  clearConfigCache();
75721
76041
  clearPackageJsonCache();
76042
+ clearPackageRoleCache();
75722
76043
  clearIgnorePatternsCache();
75723
- clearAutoSuppressionCaches();
75724
76044
  clearMinifiedFileCache();
75725
76045
  projects = null;
75726
76046
  }
@@ -76116,11 +76436,9 @@ const createScheduler = (options) => {
76116
76436
  timers.clear();
76117
76437
  queue.length = 0;
76118
76438
  };
76119
- const pendingCount = () => queue.length + running + timers.size;
76120
76439
  return {
76121
76440
  enqueue,
76122
76441
  cancelProject,
76123
- pendingCount,
76124
76442
  dispose
76125
76443
  };
76126
76444
  };
@@ -76767,8 +77085,11 @@ const METRIC = {
76767
77085
  scanPhaseDuration: "scan.phase_duration",
76768
77086
  scanFiles: "scan.files",
76769
77087
  scanScore: "scan.score",
77088
+ scanScoreRetry: "scan.score_retry",
76770
77089
  scanClean: "scan.clean",
76771
77090
  scanCheckSkipped: "scan.check_skipped",
77091
+ scanProjectSkipped: "scan.project_skipped",
77092
+ scanWorkspaceDeadCodeShared: "scan.workspace_deadcode_shared",
76772
77093
  scanNoReactDetected: "scan.no_react_detected",
76773
77094
  baselineDegraded: "baseline.degraded",
76774
77095
  ruleFired: "rule.fired",
@@ -76795,9 +77116,14 @@ const METRIC = {
76795
77116
  lspScanCompleted: "lsp.scan.completed",
76796
77117
  lspScanDuration: "lsp.scan.duration",
76797
77118
  lspScanDiagnostics: "lsp.scan.diagnostics",
76798
- tuiActionMenuOpened: "tui.action_menu_opened",
76799
77119
  tuiCompactReportShown: "tui.compact_report_shown",
76800
77120
  tuiFindingNavigated: "tui.finding_navigated",
77121
+ tuiIssueStreamShown: "tui.issue_stream_shown",
77122
+ tuiProjectPathContextShown: "tui.project_path_context_shown",
77123
+ tuiProjectSelectInlineShown: "tui.project_select_inline_shown",
77124
+ tuiReportActionSelected: "tui.report_action_selected",
77125
+ tuiCancelled: "tui.cancelled",
77126
+ tuiScanInlineShown: "tui.scan_inline_shown",
76801
77127
  tuiStackedReportCapped: "tui.stacked_report_capped",
76802
77128
  aiTrainingWarningShown: "ai.training.warning_shown",
76803
77129
  jsonOutUsed: "json.out_used"
@@ -76812,6 +77138,9 @@ const METRIC = {
76812
77138
  */
76813
77139
  const toCategoryKey = (category) => category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
76814
77140
  //#endregion
77141
+ //#region src/cli/utils/is-env-flag-enabled.ts
77142
+ const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
77143
+ //#endregion
76815
77144
  //#region src/cli/utils/anonymize-text.ts
76816
77145
  /**
76817
77146
  * Free-text fields can carry both a home-directory path (the OS username) and a
@@ -76972,7 +77301,6 @@ const resolveTracesSampleRate = () => {
76972
77301
  * session.
76973
77302
  */
76974
77303
  let lspTracesSampleRate = 0;
76975
- const isEnvFlagEnabled = (value) => value === "1" || value?.toLowerCase() === "true";
76976
77304
  const nodeMajorVersion = () => Number.parseInt(process.versions.node.split(".", 1)[0] ?? "", 10) || 0;
76977
77305
  const shouldEnableLspTelemetry = () => {
76978
77306
  if (isEnvFlagEnabled(process.env.REACT_DOCTOR_NO_TELEMETRY)) return false;
@@ -77100,5 +77428,5 @@ const startLanguageServer = () => {
77100
77428
  };
77101
77429
  //#endregion
77102
77430
  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
77431
+ !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]="383b8ad0-ef31-54a1-9d90-cdefff0fb2cb")}catch(e){}}();
77432
+ //# debugId=383b8ad0-ef31-54a1-9d90-cdefff0fb2cb