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/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0c9da9c7-4847-5807-983e-a57b7eec97cd")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e18e3c42-e18e-5087-9b71-eb1bcb203d6b")}catch(e){}}();
3
3
  import { i as __toESM$1, r as __require, t as __commonJSMin$1 } from "./chunk-ChKA3wi6.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { ALL_REACT_DOCTOR_RULE_KEYS, CROSS_FILE_DEPENDENCY_COLLECTORS, CROSS_FILE_RULE_IDS, FRAMEWORK_SPECIFIC_RULE_KEYS, FRAMEWORK_TOKENS, MOTION_LIBRARY_PACKAGES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_DOCTOR_RULE_REGISTRY, REACT_DOCTOR_SCAN_RULES, classifySecurityScanFile, collectCrossFileDependencyProbes, resetManifestCaches, shouldReadSecurityScanContent } from "oxlint-plugin-react-doctor/core";
6
6
  import * as Crypto from "node:crypto";
7
7
  import crypto, { createHash } from "node:crypto";
8
- import * as NFS from "node:fs";
9
- import fs from "node:fs";
10
8
  import * as Path from "node:path";
11
9
  import path from "node:path";
10
+ import * as NFS from "node:fs";
11
+ import fs from "node:fs";
12
12
  import * as NodeChildProcess from "node:child_process";
13
- import { execFileSync, spawn, spawnSync } from "node:child_process";
13
+ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
14
14
  import { ResolverFactory } from "oxc-resolver";
15
15
  import ts from "typescript";
16
16
  import * as NodeUrl from "node:url";
@@ -3621,7 +3621,7 @@ const flatMap$5 = /* @__PURE__ */ dual(2, (self, f) => isNone(self) ? none() : f
3621
3621
  * @category filtering
3622
3622
  * @since 2.0.0
3623
3623
  */
3624
- const filter = /* @__PURE__ */ dual(2, (self, predicate) => isNone(self) ? none() : predicate(self.value) ? some(self.value) : none());
3624
+ const filter$1 = /* @__PURE__ */ dual(2, (self, predicate) => isNone(self) ? none() : predicate(self.value) ? some(self.value) : none());
3625
3625
  //#endregion
3626
3626
  //#region ../../node_modules/.pnpm/effect@4.0.0-beta.70/node_modules/effect/dist/Result.js
3627
3627
  /**
@@ -15556,7 +15556,7 @@ function transformOptional(f) {
15556
15556
  */
15557
15557
  function withDefault$1(defaultValue) {
15558
15558
  return new Getter((o) => {
15559
- const filtered = filter(o, isNotUndefined);
15559
+ const filtered = filter$1(o, isNotUndefined);
15560
15560
  return isSome(filtered) ? succeed$2(filtered) : mapEager(defaultValue, some);
15561
15561
  });
15562
15562
  }
@@ -19404,6 +19404,10 @@ var JsonReportProjectEntryV3 = class extends Class("JsonReportProjectEntryV3")({
19404
19404
  scannedFileCount: optional(Number$1),
19405
19405
  elapsedMilliseconds: Number$1
19406
19406
  }) {};
19407
+ var JsonReportSkippedProject = class extends Class("JsonReportSkippedProject")({
19408
+ directory: String$1,
19409
+ reason: Literal("max-duration")
19410
+ }) {};
19407
19411
  /** Original full, diff, and staged report contract. */
19408
19412
  var JsonReportV1 = class extends Class("JsonReportV1")({
19409
19413
  schemaVersion: Literal(1),
@@ -19432,6 +19436,7 @@ var JsonReportV1 = class extends Class("JsonReportV1")({
19432
19436
  reactDetected: optional(Boolean$1),
19433
19437
  diff: NullOr(JsonReportDiffInfo),
19434
19438
  projects: ArraySchema(JsonReportProjectEntry),
19439
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19435
19440
  diagnostics: ArraySchema(Diagnostic),
19436
19441
  summary: JsonReportSummary,
19437
19442
  elapsedMilliseconds: Number$1,
@@ -19460,6 +19465,7 @@ var JsonReportV2 = class extends Class("JsonReportV2")({
19460
19465
  diff: NullOr(JsonReportDiffInfo),
19461
19466
  baseline: JsonReportBaseline,
19462
19467
  projects: ArraySchema(JsonReportProjectEntry),
19468
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19463
19469
  diagnostics: ArraySchema(Diagnostic),
19464
19470
  summary: JsonReportSummary,
19465
19471
  elapsedMilliseconds: Number$1,
@@ -19476,6 +19482,7 @@ var JsonReportV3 = class extends Class("JsonReportV3")({
19476
19482
  diff: NullOr(JsonReportDiffInfo),
19477
19483
  baseline: optional(JsonReportBaseline),
19478
19484
  projects: ArraySchema(JsonReportProjectEntryV3),
19485
+ skippedProjects: optional(ArraySchema(JsonReportSkippedProject)),
19479
19486
  diagnostics: ArraySchema(JsonReportDiagnosticV3),
19480
19487
  summary: JsonReportSummary,
19481
19488
  elapsedMilliseconds: Number$1,
@@ -53458,6 +53465,47 @@ const flattenArray = (self) => transformPull$1(self, (pull) => {
53458
53465
  }));
53459
53466
  });
53460
53467
  /**
53468
+ * Filters arrays of elements emitted by a channel, applying the filter
53469
+ * to each element within the arrays and only emitting non-empty filtered arrays.
53470
+ *
53471
+ * **Example** (Filtering array output)
53472
+ *
53473
+ * ```ts
53474
+ * import { Array, Channel } from "effect"
53475
+ *
53476
+ * const nonEmptyArrayPredicate = Array.isReadonlyArrayNonEmpty
53477
+ *
53478
+ * // Create a channel that outputs arrays of mixed data
53479
+ * const arrayChannel = Channel.fromIterable([
53480
+ * Array.make(1, 2, 3, 4, 5),
53481
+ * Array.make(6, 7, 8, 9, 10),
53482
+ * Array.make(11, 12, 13, 14, 15)
53483
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
53484
+ *
53485
+ * // Filter arrays to keep only even numbers
53486
+ * const evenArraysChannel = Channel.filterArray(arrayChannel, (n) => n % 2 === 0)
53487
+ * // Outputs: [2, 4], [6, 8, 10], [12, 14]
53488
+ * // Note: Only non-empty filtered arrays are emitted
53489
+ *
53490
+ * // Arrays that would become empty after filtering are discarded entirely
53491
+ * const oddChannel = Channel.fromIterable([
53492
+ * Array.make(1, 3, 5),
53493
+ * Array.make(2, 4),
53494
+ * Array.make(7, 9)
53495
+ * ]).pipe(Channel.filter(nonEmptyArrayPredicate))
53496
+ * const filteredOddChannel = Channel.filterArray(oddChannel, (n) => n % 2 === 0)
53497
+ * // Outputs: [2, 4] (the arrays [1,3,5] and [7,9] are discarded)
53498
+ * ```
53499
+ *
53500
+ * @category filtering
53501
+ * @since 4.0.0
53502
+ */
53503
+ const filterArray = /* @__PURE__ */ dual(2, (self, predicate) => transformPull$1(self, (pull) => succeed$2(flatMap$2(pull, function loop(arr) {
53504
+ const passes = [];
53505
+ for (let i = 0; i < arr.length; i++) if (predicate(arr[i])) passes.push(arr[i]);
53506
+ return isReadonlyArrayNonEmpty(passes) ? succeed$2(passes) : flatMap$2(pull, loop);
53507
+ }))));
53508
+ /**
53461
53509
  * Filters and maps each element inside emitted non-empty arrays using a
53462
53510
  * `Filter`.
53463
53511
  *
@@ -55095,6 +55143,30 @@ const concat = /* @__PURE__ */ dual(2, (self, that) => flatten(fromArray([self,
55095
55143
  */
55096
55144
  const merge$1 = /* @__PURE__ */ dual((args) => isStream(args[0]) && isStream(args[1]), (self, that, options) => fromChannel(merge$2(toChannel(self), toChannel(that), options)));
55097
55145
  /**
55146
+ * Filters a stream to the elements that satisfy a predicate.
55147
+ *
55148
+ * **Example** (Filtering stream values)
55149
+ *
55150
+ * ```ts
55151
+ * import { Console, Effect, Stream } from "effect"
55152
+ *
55153
+ * const program = Effect.gen(function*() {
55154
+ * const stream = Stream.make(1, 2, 3, 4).pipe(
55155
+ * Stream.filter((n) => n % 2 === 0)
55156
+ * )
55157
+ * const values = yield* Stream.runCollect(stream)
55158
+ * yield* Console.log(values)
55159
+ * })
55160
+ *
55161
+ * Effect.runPromise(program)
55162
+ * // Output: [ 2, 4 ]
55163
+ * ```
55164
+ *
55165
+ * @category filtering
55166
+ * @since 2.0.0
55167
+ */
55168
+ const filter = /* @__PURE__ */ dual(2, (self, predicate) => fromChannel(filterArray(toChannel(self), predicate)));
55169
+ /**
55098
55170
  * Filters and maps stream elements in one pass using a `Filter`.
55099
55171
  *
55100
55172
  * @category filtering
@@ -61878,6 +61950,7 @@ const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.m?js$/i;
61878
61950
  const GENERATED_SOURCE_DIRECTORY_PATTERN = /(?:^|\/)__generated__\//;
61879
61951
  const MINIFIED_SNIFF_BYTES = 65536;
61880
61952
  const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
61953
+ const GIT_CHECK_ATTR_MAX_BUFFER_BYTES = GIT_LS_FILES_MAX_BUFFER_BYTES;
61881
61954
  const SCANNED_DOT_DIRECTORIES = new Set([".dumi", ".storybook"]);
61882
61955
  const IGNORED_DIRECTORIES = new Set([
61883
61956
  ".angular",
@@ -62163,13 +62236,9 @@ const DEAD_CODE_WORKER_TIMEOUT_MS = 12e4;
62163
62236
  const OXLINT_SPLIT_TOTAL_BUDGET_MS = 18e4;
62164
62237
  const ABORT_EXIT_CODES = new Set([134, 3221226505]);
62165
62238
  const OXLINT_OOM_RESCUE_BUDGET_MS = 6e4;
62166
- const DEAD_CODE_PHASE_TIMEOUT_MS = 15e4;
62167
- const LINT_PHASE_TIMEOUT_MS = 3e5;
62168
- const SCAN_TOTAL_DEADLINE_MS = 9e5;
62169
62239
  const DEAD_CODE_WORKER_MAX_OLD_SPACE_MB = 8192;
62170
62240
  const DEAD_CODE_WORKER_MEM_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
62171
62241
  const DEAD_CODE_TIMEOUT_CEILING_MS = 6e5;
62172
- const DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS = 3e4;
62173
62242
  const DEAD_CODE_OVERLAP_PARSE_SHARE = .4;
62174
62243
  const RECOMMENDED_PNPM_MINIMUM_RELEASE_AGE_MINUTES = 10080;
62175
62244
  const REACT_SERVER_DOM_PACKAGES = [
@@ -62216,7 +62285,7 @@ const SOCKET_FREE_USER_AGENT = "react-doctor-supply-chain";
62216
62285
  const FILE_LINT_CACHE_FILENAME = "file-lint-cache.json";
62217
62286
  const FILE_LINT_CACHE_MAX_FILE_COUNT = 5e4;
62218
62287
  const SIDECAR_LINT_CACHE_FILENAME = "sidecar-lint-cache.json";
62219
- const CORE_PACKAGE_VERSION = "0.9.3";
62288
+ const CORE_PACKAGE_VERSION = "0.9.4";
62220
62289
  const DEAD_CODE_CACHE_FILENAME = "dead-code-cache.json";
62221
62290
  const DEAD_CODE_SUMMARY_CACHE_FILENAME = "dead-code-summaries.json";
62222
62291
  const SUPPLY_CHAIN_PLUGIN = "socket";
@@ -62264,6 +62333,66 @@ const isPlainObject = (value) => {
62264
62333
  const prototype = Object.getPrototypeOf(value);
62265
62334
  return prototype === null || prototype === Object.prototype;
62266
62335
  };
62336
+ const LINGUIST_ATTRIBUTES = ["linguist-generated", "linguist-vendored"];
62337
+ const DISABLED_ATTRIBUTE_VALUES = new Set([
62338
+ "unspecified",
62339
+ "unset",
62340
+ "false",
62341
+ "0",
62342
+ "off",
62343
+ "no"
62344
+ ]);
62345
+ const buildInput = (relativePaths) => `${relativePaths.join("\0")}\0`;
62346
+ const parseIgnoredPaths = (output) => {
62347
+ const ignoredPaths = /* @__PURE__ */ new Set();
62348
+ const fields = output.split("\0");
62349
+ for (let fieldIndex = 0; fieldIndex + 2 < fields.length; fieldIndex += 3) {
62350
+ const filePath = fields[fieldIndex];
62351
+ const value = fields[fieldIndex + 2]?.toLowerCase();
62352
+ if (filePath && value && !DISABLED_ATTRIBUTE_VALUES.has(value)) ignoredPaths.add(filePath);
62353
+ }
62354
+ return ignoredPaths;
62355
+ };
62356
+ const collectGitLinguistIgnoredPaths = (rootDirectory, relativePaths) => {
62357
+ if (relativePaths.length === 0) return /* @__PURE__ */ new Set();
62358
+ const result = spawnSync("git", [
62359
+ "check-attr",
62360
+ "-z",
62361
+ "--stdin",
62362
+ ...LINGUIST_ATTRIBUTES
62363
+ ], {
62364
+ cwd: rootDirectory,
62365
+ encoding: "utf-8",
62366
+ input: buildInput(relativePaths),
62367
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES
62368
+ });
62369
+ if (result.error || result.status !== 0) return /* @__PURE__ */ new Set();
62370
+ return parseIgnoredPaths(result.stdout);
62371
+ };
62372
+ const collectGitLinguistIgnoredPathsCooperative = (rootDirectory, relativePaths, signal) => {
62373
+ if (relativePaths.length === 0) return Promise.resolve(/* @__PURE__ */ new Set());
62374
+ return new Promise((resolve, reject) => {
62375
+ signal?.throwIfAborted();
62376
+ execFile("git", [
62377
+ "check-attr",
62378
+ "-z",
62379
+ "--stdin",
62380
+ ...LINGUIST_ATTRIBUTES
62381
+ ], {
62382
+ cwd: rootDirectory,
62383
+ encoding: "utf-8",
62384
+ killSignal: "SIGKILL",
62385
+ maxBuffer: GIT_CHECK_ATTR_MAX_BUFFER_BYTES,
62386
+ signal
62387
+ }, (error, stdout) => {
62388
+ if (signal?.aborted) {
62389
+ reject(signal.reason);
62390
+ return;
62391
+ }
62392
+ resolve(error ? /* @__PURE__ */ new Set() : parseIgnoredPaths(stdout));
62393
+ }).stdin?.end(buildInput(relativePaths));
62394
+ });
62395
+ };
62267
62396
  const lastSourceMappingUrl = (fileContent) => {
62268
62397
  const lastReference = [...fileContent.matchAll(/\/\/# sourceMappingURL=(\S+)/g)].at(-1);
62269
62398
  return lastReference ? lastReference[1] : null;
@@ -62368,6 +62497,9 @@ function* walkSourceTreeFiles(rootDirectory) {
62368
62497
  }
62369
62498
  }
62370
62499
  }
62500
+ const yieldToEventLoop = () => new Promise((resolve) => {
62501
+ setImmediate(resolve);
62502
+ });
62371
62503
  const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
62372
62504
  const entries = [];
62373
62505
  for (const relativePath of relativePaths) {
@@ -62381,23 +62513,55 @@ const collectSizedSourceFiles = (rootDirectory, relativePaths) => {
62381
62513
  }
62382
62514
  return entries;
62383
62515
  };
62384
- const gitLsFiles = (rootDirectory, flags) => {
62516
+ const collectSizedSourceFilesCooperative = async (rootDirectory, relativePaths, signal) => {
62517
+ const entries = [];
62518
+ let sliceStartedAt = performance.now();
62519
+ for (const relativePath of relativePaths) {
62520
+ signal?.throwIfAborted();
62521
+ const absolutePath = Path.resolve(rootDirectory, relativePath);
62522
+ const sizeBytes = statSourceFileSize(absolutePath);
62523
+ if (!isLargeMinifiedFile(absolutePath, sizeBytes)) entries.push({
62524
+ path: relativePath,
62525
+ sizeBytes: sizeBytes ?? 0
62526
+ });
62527
+ if (performance.now() - sliceStartedAt >= 12) {
62528
+ await yieldToEventLoop();
62529
+ sliceStartedAt = performance.now();
62530
+ }
62531
+ }
62532
+ return entries;
62533
+ };
62534
+ const parseGitSourceFilePaths = (output) => {
62535
+ const trackedPaths = /* @__PURE__ */ new Set();
62536
+ const untrackedPaths = [];
62537
+ for (const entry of output.split("\0")) {
62538
+ if (entry.length === 0) continue;
62539
+ const trackedPath = /^[0-7]{6} [0-9a-f]+ [0-3]\t([\s\S]+)$/.exec(entry)?.[1];
62540
+ if (trackedPath === void 0) untrackedPaths.push(entry);
62541
+ else trackedPaths.add(trackedPath);
62542
+ }
62543
+ return {
62544
+ trackedPaths: [...trackedPaths],
62545
+ untrackedPaths
62546
+ };
62547
+ };
62548
+ const listGitSourceFilePaths = (rootDirectory) => {
62385
62549
  const result = spawnSync("git", [
62386
62550
  "ls-files",
62387
62551
  "-z",
62388
- ...flags
62552
+ "--stage",
62553
+ "--others",
62554
+ "--exclude-standard"
62389
62555
  ], {
62390
62556
  cwd: rootDirectory,
62391
62557
  encoding: "utf-8",
62392
62558
  maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
62393
62559
  });
62394
62560
  if (result.error || result.status !== 0) return null;
62395
- return result.stdout.split("\0").filter((filePath) => filePath.length > 0);
62561
+ return parseGitSourceFilePaths(result.stdout);
62396
62562
  };
62397
- const listSourceFilesViaGit = (rootDirectory) => {
62398
- const trackedPaths = gitLsFiles(rootDirectory, ["--cached"]);
62399
- const untrackedPaths = gitLsFiles(rootDirectory, ["--others", "--exclude-standard"]);
62400
- if (trackedPaths === null || untrackedPaths === null) return null;
62563
+ const collectGitCandidateSourceFilePaths = (rootDirectory, paths) => {
62564
+ const { trackedPaths, untrackedPaths } = paths;
62401
62565
  const emitDuplicateJsPaths = collectTypeScriptEmitDuplicateJsPaths({
62402
62566
  trackedPaths: new Set(trackedPaths),
62403
62567
  untrackedPaths,
@@ -62405,12 +62569,65 @@ const listSourceFilesViaGit = (rootDirectory) => {
62405
62569
  });
62406
62570
  return [...trackedPaths, ...untrackedPaths].filter((filePath) => isLintableSourceFile(filePath) && !hasIgnoredPathSegment(filePath) && !emitDuplicateJsPaths.has(filePath));
62407
62571
  };
62572
+ const filterGitSourceFilePaths = (rootDirectory, paths) => {
62573
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
62574
+ const linguistIgnoredPaths = collectGitLinguistIgnoredPaths(rootDirectory, candidatePaths);
62575
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
62576
+ };
62577
+ const listSourceFilesViaGit = (rootDirectory) => {
62578
+ const paths = listGitSourceFilePaths(rootDirectory);
62579
+ return paths === null ? null : filterGitSourceFilePaths(rootDirectory, paths);
62580
+ };
62581
+ const listSourceFilesViaGitCooperative = async (rootDirectory, signal) => {
62582
+ const paths = await new Promise((resolve, reject) => {
62583
+ signal?.throwIfAborted();
62584
+ execFile("git", [
62585
+ "ls-files",
62586
+ "-z",
62587
+ "--stage",
62588
+ "--others",
62589
+ "--exclude-standard"
62590
+ ], {
62591
+ cwd: rootDirectory,
62592
+ encoding: "utf-8",
62593
+ killSignal: "SIGKILL",
62594
+ maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
62595
+ signal
62596
+ }, (error, stdout) => {
62597
+ if (signal?.aborted) {
62598
+ reject(signal.reason);
62599
+ return;
62600
+ }
62601
+ resolve(error ? null : parseGitSourceFilePaths(stdout));
62602
+ });
62603
+ });
62604
+ if (paths === null) return null;
62605
+ const candidatePaths = collectGitCandidateSourceFilePaths(rootDirectory, paths);
62606
+ const linguistIgnoredPaths = await collectGitLinguistIgnoredPathsCooperative(rootDirectory, candidatePaths, signal);
62607
+ return candidatePaths.filter((filePath) => !linguistIgnoredPaths.has(filePath));
62608
+ };
62408
62609
  const listSourceFilesViaFilesystem = (rootDirectory) => {
62409
62610
  const filePaths = [];
62410
62611
  for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
62411
62612
  return filePaths;
62412
62613
  };
62413
- const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort());
62614
+ const listSourceFilesViaFilesystemCooperative = async (rootDirectory, signal) => {
62615
+ const filePaths = [];
62616
+ let sliceStartedAt = performance.now();
62617
+ for (const { absolutePath, name } of walkSourceTreeFiles(rootDirectory)) {
62618
+ signal?.throwIfAborted();
62619
+ if (isLintableSourceFile(name)) filePaths.push(Path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
62620
+ if (performance.now() - sliceStartedAt >= 12) {
62621
+ await yieldToEventLoop();
62622
+ sliceStartedAt = performance.now();
62623
+ }
62624
+ }
62625
+ return filePaths;
62626
+ };
62627
+ const listSourceFilePaths = (rootDirectory) => (listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory)).sort();
62628
+ const listSourceFilePathsCooperative = async (rootDirectory, signal) => (await listSourceFilesViaGitCooperative(rootDirectory, signal) ?? await listSourceFilesViaFilesystemCooperative(rootDirectory, signal)).sort();
62629
+ const listSourceFilesWithSize = (rootDirectory) => collectSizedSourceFiles(rootDirectory, listSourceFilePaths(rootDirectory));
62630
+ const listSourceFilesCooperative = async (rootDirectory, signal) => (await collectSizedSourceFilesCooperative(rootDirectory, await listSourceFilePathsCooperative(rootDirectory, signal), signal)).map((entry) => entry.path);
62414
62631
  const listSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).map((entry) => entry.path);
62415
62632
  const countSourceFiles = (rootDirectory) => listSourceFilesWithSize(rootDirectory).length;
62416
62633
  const UTF8_BOM_CHARACTER = "";
@@ -63988,13 +64205,7 @@ const hasReactDependency = (packageJson) => {
63988
64205
  };
63989
64206
  return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
63990
64207
  };
63991
- const getPreactVersion = (packageJson) => {
63992
- return {
63993
- ...packageJson.peerDependencies,
63994
- ...packageJson.dependencies,
63995
- ...packageJson.devDependencies
63996
- }.preact ?? null;
63997
- };
64208
+ const getPreactVersion = (packageJson) => getDependencySpec(packageJson, "preact");
63998
64209
  const resolveCatalogBackedDependencyVersion = ({ rootDirectory, rootPackageJson, packageName, sourceDirectory = rootDirectory, sourcePackageJson = rootPackageJson, version }) => {
63999
64210
  if (version === null || !isCatalogReference(version)) return version;
64000
64211
  const catalogName = extractCatalogName(version);
@@ -64739,7 +64950,16 @@ const getBrowserslistProtectionRequirement = (directory) => {
64739
64950
  if (!queries) return void 0;
64740
64951
  let requirement;
64741
64952
  let hasUnsupportedTarget = false;
64742
- for (const browserTarget of (0, import_browserslist.default)(queries)) {
64953
+ const previousIgnoreOldData = process.env.BROWSERSLIST_IGNORE_OLD_DATA;
64954
+ let browserTargets = [];
64955
+ try {
64956
+ process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true";
64957
+ browserTargets = (0, import_browserslist.default)(queries);
64958
+ } finally {
64959
+ if (previousIgnoreOldData === void 0) delete process.env.BROWSERSLIST_IGNORE_OLD_DATA;
64960
+ else process.env.BROWSERSLIST_IGNORE_OLD_DATA = previousIgnoreOldData;
64961
+ }
64962
+ for (const browserTarget of browserTargets) {
64743
64963
  const browserRequirement = getBrowserProtectionRequirement(browserTarget);
64744
64964
  if (browserRequirement === "unsupported") hasUnsupportedTarget = true;
64745
64965
  if (browserRequirement === "noreferrer") requirement = "noreferrer";
@@ -64849,9 +65069,9 @@ const clearProjectCache = () => {
64849
65069
  * framework-agnostic rules. Throws only when the directory has nothing
64850
65070
  * to scan (no enclosing project and no source files of its own).
64851
65071
  */
64852
- const discoverProjectWithoutPackageJson = (directory) => {
64853
- const sourceFileCount = countSourceFiles(directory);
64854
- const hasOwnTsConfig = NFS.existsSync(Path.join(directory, "tsconfig.json"));
65072
+ const discoverProjectWithoutPackageJson = (directory, options) => {
65073
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
65074
+ const hasOwnTsConfig = isFile(Path.join(directory, "tsconfig.json"));
64855
65075
  const enclosingProjectRoot = findNearestAncestorPackageJson(directory);
64856
65076
  const enclosingProject = enclosingProjectRoot !== null ? discoverProject(enclosingProjectRoot) : null;
64857
65077
  if (enclosingProject !== null) return {
@@ -64917,12 +65137,15 @@ const discoverProjectWithoutPackageJson = (directory) => {
64917
65137
  sourceFileCount
64918
65138
  };
64919
65139
  };
64920
- const discoverProject = (directory) => {
65140
+ const discoverProject = (directory, options = {}) => {
64921
65141
  const cached = cachedProjectInfos.get(directory);
64922
- if (cached !== void 0) return cached;
65142
+ if (cached !== void 0) return options.sourceFileCount === void 0 ? cached : {
65143
+ ...cached,
65144
+ sourceFileCount: options.sourceFileCount
65145
+ };
64923
65146
  const packageJsonPath = Path.join(directory, "package.json");
64924
65147
  if (!isFile(packageJsonPath)) {
64925
- const synthesized = discoverProjectWithoutPackageJson(directory);
65148
+ const synthesized = discoverProjectWithoutPackageJson(directory, options);
64926
65149
  cachedProjectInfos.set(directory, synthesized);
64927
65150
  return synthesized;
64928
65151
  }
@@ -64984,8 +65207,8 @@ const discoverProject = (directory) => {
64984
65207
  const tailwindVersion = tailwindcss.version;
64985
65208
  const zodVersion = zod.version;
64986
65209
  const projectName = packageJson.name ?? Path.basename(directory);
64987
- const hasTypeScript = NFS.existsSync(Path.join(directory, "tsconfig.json"));
64988
- const sourceFileCount = countSourceFiles(directory);
65210
+ const hasTypeScript = isFile(Path.join(directory, "tsconfig.json"));
65211
+ const sourceFileCount = options.sourceFileCount ?? countSourceFiles(directory);
64989
65212
  const hasReactNativeWorkspace = framework === "expo" || framework === "react-native" || workspaceFacts.hasReactNativeAwarePackage;
64990
65213
  const expoVersion = hasReactNativeWorkspace ? resolveCatalogBackedDependencyVersion({
64991
65214
  rootDirectory: directory,
@@ -65114,134 +65337,6 @@ const discoverProject = (directory) => {
65114
65337
  * single source file has nothing to analyze.
65115
65338
  */
65116
65339
  const isAnalyzableProject = (project) => project.reactVersion !== null || project.preactVersion !== null || project.sourceFileCount > 0;
65117
- const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
65118
- var InvalidGlobPatternError = class extends Error {
65119
- pattern;
65120
- reason;
65121
- constructor(pattern, reason) {
65122
- super(`Invalid glob pattern ${JSON.stringify(pattern)}: ${reason}`);
65123
- this.name = "InvalidGlobPatternError";
65124
- this.pattern = pattern;
65125
- this.reason = reason;
65126
- }
65127
- };
65128
- const assertGlobPattern = (condition, pattern, reason) => {
65129
- if (!condition) throw new InvalidGlobPatternError(pattern, reason);
65130
- };
65131
- const countGlobWildcards = (pattern) => (pattern.match(/[*?]/g) ?? []).length;
65132
- const normalizeGlobPattern = (pattern) => pattern.replace(/\\/g, "/").replace(/^\//, "");
65133
- const PICOMATCH_OPTIONS = {
65134
- dot: true,
65135
- strictSlashes: false,
65136
- windows: false
65137
- };
65138
- const compileGlobPattern = (rawPattern) => {
65139
- assertGlobPattern(typeof rawPattern === "string" && rawPattern.length > 0, String(rawPattern), "pattern must be a non-empty string.");
65140
- assertGlobPattern(rawPattern.length <= MAX_GLOB_PATTERN_LENGTH_CHARS, rawPattern, `pattern length ${rawPattern.length} exceeds the maximum of ${MAX_GLOB_PATTERN_LENGTH_CHARS} characters.`);
65141
- const wildcardCount = countGlobWildcards(rawPattern);
65142
- 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.`);
65143
- try {
65144
- return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
65145
- } catch (caughtError) {
65146
- throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
65147
- }
65148
- };
65149
- const compileGlobPatternsLenient = (patterns, onInvalid) => {
65150
- const compiled = [];
65151
- for (const pattern of patterns) try {
65152
- compiled.push(compileGlobPattern(pattern));
65153
- } catch (caughtError) {
65154
- if (!(caughtError instanceof InvalidGlobPatternError)) throw caughtError;
65155
- onInvalid(caughtError);
65156
- }
65157
- return compiled;
65158
- };
65159
- const toRelativePath = (filePath, rootDirectory) => {
65160
- const normalizedFilePath = filePath.replace(/\\/g, "/");
65161
- const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
65162
- if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
65163
- return normalizedFilePath.replace(/^\.\//, "");
65164
- };
65165
- const warnConfigIssue = (message) => {
65166
- process.stderr.write(`[react-doctor] ${message}\n`);
65167
- };
65168
- const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
65169
- const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65170
- const validateOverrideEntry = (entry, index) => {
65171
- if (!isPlainObject(entry)) {
65172
- warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
65173
- return null;
65174
- }
65175
- if (!isStringArray(entry.files)) {
65176
- warnConfigIssue(`ignore.overrides[${index}].files must be an array of strings; ignoring this entry.`);
65177
- return null;
65178
- }
65179
- if (entry.rules !== void 0 && !isStringArray(entry.rules)) {
65180
- 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).`);
65181
- return { files: entry.files };
65182
- }
65183
- return entry.rules === void 0 ? { files: entry.files } : {
65184
- files: entry.files,
65185
- rules: entry.rules
65186
- };
65187
- };
65188
- const compileIgnoreOverrides = (userConfig) => {
65189
- const overrides = userConfig?.ignore?.overrides;
65190
- if (overrides === void 0) return [];
65191
- if (!Array.isArray(overrides)) {
65192
- warnConfigIssue(`ignore.overrides must be an array of { files, rules } entries; ignoring.`);
65193
- return [];
65194
- }
65195
- return overrides.flatMap((entry, index) => {
65196
- const validated = validateOverrideEntry(entry, index);
65197
- if (!validated) return [];
65198
- const filePatterns = compileGlobPatternsLenient(collectStringList(validated.files), (error) => warnConfigIssue(`ignore.overrides[${index}]: ${error.message}`));
65199
- if (filePatterns.length === 0) return [];
65200
- return [{
65201
- filePatterns,
65202
- ruleIds: new Set(collectStringList(validated.rules))
65203
- }];
65204
- });
65205
- };
65206
- const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) => {
65207
- if (overrides.length === 0) return false;
65208
- const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
65209
- const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
65210
- return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && (override.ruleIds.size === 0 || override.ruleIds.has(ruleIdentifier)));
65211
- };
65212
- const SEVERITY_FOR_OVERRIDE = {
65213
- error: "error",
65214
- warn: "warning"
65215
- };
65216
- const restampSeverity = (diagnostic, override) => {
65217
- const targetSeverity = SEVERITY_FOR_OVERRIDE[override];
65218
- if (diagnostic.severity === targetSeverity) return diagnostic;
65219
- return {
65220
- ...diagnostic,
65221
- severity: targetSeverity
65222
- };
65223
- };
65224
- /**
65225
- * Assembles the internal `RuleSeverityControls` shape from a user
65226
- * config's top-level `rules` and `categories` fields — the
65227
- * ESLint / oxlint-shaped severity surface.
65228
- *
65229
- * Returns `undefined` when neither field is present so the common
65230
- * path (no severity config at all) stays allocation-free for
65231
- * downstream consumers.
65232
- */
65233
- const buildRuleSeverityControls = (config) => {
65234
- if (!config) return void 0;
65235
- if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
65236
- return {
65237
- ...config.rules !== void 0 ? { rules: config.rules } : {},
65238
- ...config.categories !== void 0 ? { categories: config.categories } : {},
65239
- ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
65240
- };
65241
- };
65242
- const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
65243
- const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
65244
- const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
65245
65340
  const LEGACY_RULE_KEY_TO_NATIVE_RULE_KEY = {
65246
65341
  "effect/no-adjust-state-on-prop-change": "react-doctor/no-adjust-state-on-prop-change",
65247
65342
  "effect/no-chain-state-updates": "react-doctor/no-chain-state-updates",
@@ -65380,6 +65475,139 @@ const getEquivalentRuleKeys = (ruleKey) => {
65380
65475
  const nativeRuleKey = canonicalizeRuleKey(ruleKey);
65381
65476
  return [nativeRuleKey, ...getLegacyRuleKeysForNative(nativeRuleKey)];
65382
65477
  };
65478
+ const messageFromUnknown = (error) => error instanceof Error ? error.message : String(error);
65479
+ var InvalidGlobPatternError = class extends Error {
65480
+ pattern;
65481
+ reason;
65482
+ constructor(pattern, reason) {
65483
+ super(`Invalid glob pattern ${JSON.stringify(pattern)}: ${reason}`);
65484
+ this.name = "InvalidGlobPatternError";
65485
+ this.pattern = pattern;
65486
+ this.reason = reason;
65487
+ }
65488
+ };
65489
+ const assertGlobPattern = (condition, pattern, reason) => {
65490
+ if (!condition) throw new InvalidGlobPatternError(pattern, reason);
65491
+ };
65492
+ const countGlobWildcards = (pattern) => (pattern.match(/[*?]/g) ?? []).length;
65493
+ const normalizeGlobPattern = (pattern) => pattern.replace(/\\/g, "/").replace(/^\//, "");
65494
+ const PICOMATCH_OPTIONS = {
65495
+ dot: true,
65496
+ strictSlashes: false,
65497
+ windows: false
65498
+ };
65499
+ const compileGlobPattern = (rawPattern) => {
65500
+ assertGlobPattern(typeof rawPattern === "string" && rawPattern.length > 0, String(rawPattern), "pattern must be a non-empty string.");
65501
+ assertGlobPattern(rawPattern.length <= MAX_GLOB_PATTERN_LENGTH_CHARS, rawPattern, `pattern length ${rawPattern.length} exceeds the maximum of ${MAX_GLOB_PATTERN_LENGTH_CHARS} characters.`);
65502
+ const wildcardCount = countGlobWildcards(rawPattern);
65503
+ 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.`);
65504
+ try {
65505
+ return import_picomatch.default.makeRe(normalizeGlobPattern(rawPattern), PICOMATCH_OPTIONS);
65506
+ } catch (caughtError) {
65507
+ throw new InvalidGlobPatternError(rawPattern, messageFromUnknown(caughtError));
65508
+ }
65509
+ };
65510
+ const compileGlobPatternsLenient = (patterns, onInvalid) => {
65511
+ const compiled = [];
65512
+ for (const pattern of patterns) try {
65513
+ compiled.push(compileGlobPattern(pattern));
65514
+ } catch (caughtError) {
65515
+ if (!(caughtError instanceof InvalidGlobPatternError)) throw caughtError;
65516
+ onInvalid(caughtError);
65517
+ }
65518
+ return compiled;
65519
+ };
65520
+ const toRelativePath = (filePath, rootDirectory) => {
65521
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
65522
+ const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
65523
+ if (normalizedFilePath.startsWith(normalizedRoot)) return normalizedFilePath.slice(normalizedRoot.length);
65524
+ return normalizedFilePath.replace(/^\.\//, "");
65525
+ };
65526
+ const warnConfigIssue = (message) => {
65527
+ process.stderr.write(`[react-doctor] ${message}\n`);
65528
+ };
65529
+ const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
65530
+ const collectStringList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
65531
+ const hasMatchingRuleOverride = (ruleIds, ruleIdentifier) => {
65532
+ if (ruleIds.size === 0) return true;
65533
+ for (const ruleId of ruleIds) if (isSameRuleKey(ruleId, ruleIdentifier)) return true;
65534
+ return false;
65535
+ };
65536
+ const validateOverrideEntry = (entry, index) => {
65537
+ if (!isPlainObject(entry)) {
65538
+ warnConfigIssue(`ignore.overrides[${index}] must be an object with { files, rules }; ignoring this entry.`);
65539
+ return null;
65540
+ }
65541
+ if (!isStringArray(entry.files)) {
65542
+ warnConfigIssue(`ignore.overrides[${index}].files must be an array of strings; ignoring this entry.`);
65543
+ return null;
65544
+ }
65545
+ if (entry.rules !== void 0 && !isStringArray(entry.rules)) {
65546
+ 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).`);
65547
+ return { files: entry.files };
65548
+ }
65549
+ return entry.rules === void 0 ? { files: entry.files } : {
65550
+ files: entry.files,
65551
+ rules: entry.rules
65552
+ };
65553
+ };
65554
+ const compileIgnoreOverrides = (userConfig) => {
65555
+ const overrides = userConfig?.ignore?.overrides;
65556
+ if (overrides === void 0) return [];
65557
+ if (!Array.isArray(overrides)) {
65558
+ warnConfigIssue(`ignore.overrides must be an array of { files, rules } entries; ignoring.`);
65559
+ return [];
65560
+ }
65561
+ return overrides.flatMap((entry, index) => {
65562
+ const validated = validateOverrideEntry(entry, index);
65563
+ if (!validated) return [];
65564
+ const filePatterns = compileGlobPatternsLenient(collectStringList(validated.files), (error) => warnConfigIssue(`ignore.overrides[${index}]: ${error.message}`));
65565
+ if (filePatterns.length === 0) return [];
65566
+ return [{
65567
+ filePatterns,
65568
+ ruleIds: new Set(collectStringList(validated.rules))
65569
+ }];
65570
+ });
65571
+ };
65572
+ const isDiagnosticIgnoredByOverrides = (diagnostic, rootDirectory, overrides) => {
65573
+ if (overrides.length === 0) return false;
65574
+ const relativeFilePath = toRelativePath(diagnostic.filePath, rootDirectory);
65575
+ const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
65576
+ return overrides.some((override) => override.filePatterns.some((pattern) => pattern.test(relativeFilePath)) && hasMatchingRuleOverride(override.ruleIds, ruleIdentifier));
65577
+ };
65578
+ const SEVERITY_FOR_OVERRIDE = {
65579
+ error: "error",
65580
+ warn: "warning"
65581
+ };
65582
+ const restampSeverity = (diagnostic, override) => {
65583
+ const targetSeverity = SEVERITY_FOR_OVERRIDE[override];
65584
+ if (diagnostic.severity === targetSeverity) return diagnostic;
65585
+ return {
65586
+ ...diagnostic,
65587
+ severity: targetSeverity
65588
+ };
65589
+ };
65590
+ /**
65591
+ * Assembles the internal `RuleSeverityControls` shape from a user
65592
+ * config's top-level `rules` and `categories` fields — the
65593
+ * ESLint / oxlint-shaped severity surface.
65594
+ *
65595
+ * Returns `undefined` when neither field is present so the common
65596
+ * path (no severity config at all) stays allocation-free for
65597
+ * downstream consumers.
65598
+ */
65599
+ const buildRuleSeverityControls = (config) => {
65600
+ if (!config) return void 0;
65601
+ if (config.rules === void 0 && config.categories === void 0 && config.buckets === void 0) return;
65602
+ return {
65603
+ ...config.rules !== void 0 ? { rules: config.rules } : {},
65604
+ ...config.categories !== void 0 ? { categories: config.categories } : {},
65605
+ ...config.buckets !== void 0 ? { buckets: config.buckets } : {}
65606
+ };
65607
+ };
65608
+ const FOREIGN_INLINE_DISABLE_PATTERN = /(?:\/\/|\/\*)[ \t]*(eslint|oxlint)-disable-(next-line|line)(?![\w-])([^\r\n]*)/;
65609
+ const FOREIGN_BLOCK_DISABLE_PATTERN = /\/\*[ \t]*(eslint|oxlint)-disable(?![\w-])([^*\r\n]*)/;
65610
+ const FOREIGN_BLOCK_ENABLE_PATTERN = /\/\*[ \t]*(?:eslint|oxlint)-enable(?![\w-])([^*\r\n]*)/;
65383
65611
  const stripDescriptionTail = (ruleList) => {
65384
65612
  const descriptionMatch = ruleList.match(/(?:^|\s)--\s/);
65385
65613
  if (!descriptionMatch || descriptionMatch.index === void 0) return ruleList;
@@ -66030,12 +66258,8 @@ const buildDiagnosticPipeline = (input) => {
66030
66258
  if (rule.tags.includes("migration-hint")) return false;
66031
66259
  return getFileContext(diagnostic.filePath) !== "production";
66032
66260
  };
66033
- const isRuleIgnored = (ruleIdentifier) => {
66034
- for (const ignored of ignoredRules) if (isSameRuleKey(ignored, ruleIdentifier)) return true;
66035
- return false;
66036
- };
66037
- const isAppOnlyRule = (ruleIdentifier) => {
66038
- for (const appOnlyRuleKey of APP_ONLY_RULE_KEYS) if (isSameRuleKey(appOnlyRuleKey, ruleIdentifier)) return true;
66261
+ const matchesRuleKey = (ruleIdentifier, candidates) => {
66262
+ for (const candidate of candidates) if (isSameRuleKey(candidate, ruleIdentifier)) return true;
66039
66263
  return false;
66040
66264
  };
66041
66265
  const isRnRawTextSuppressedByConfig = (diagnostic) => {
@@ -66077,12 +66301,13 @@ const buildDiagnosticPipeline = (input) => {
66077
66301
  if (explicitSeverityOverride === "off") return suppress(current, "config");
66078
66302
  if (explicitSeverityOverride !== void 0) current = restampSeverity(current, explicitSeverityOverride);
66079
66303
  }
66304
+ const ruleIdentifier = `${current.plugin}/${current.rule}`;
66080
66305
  if (explicitRuleOverride === void 0) {
66081
- if (isAppOnlyRule(`${current.plugin}/${current.rule}`) && isLibraryFile(current.filePath)) return null;
66306
+ if (matchesRuleKey(ruleIdentifier, APP_ONLY_RULE_KEYS) && isLibraryFile(current.filePath)) return null;
66082
66307
  }
66083
66308
  if (!showWarnings && current.severity === "warning" && explicitSeverityOverride !== "warn") return null;
66084
66309
  if (userConfig) {
66085
- if (isRuleIgnored(`${current.plugin}/${current.rule}`)) return suppress(current, "config");
66310
+ if (matchesRuleKey(ruleIdentifier, ignoredRules)) return suppress(current, "config");
66086
66311
  if (isFileIgnoredByPatterns(current.filePath, rootDirectory, ignoredFilePatterns)) return null;
66087
66312
  if (isDiagnosticIgnoredByOverrides(current, rootDirectory, compiledOverrides)) return suppress(current, "override");
66088
66313
  if (isRnRawTextSuppressedByConfig(current)) return null;
@@ -66091,7 +66316,6 @@ const buildDiagnosticPipeline = (input) => {
66091
66316
  if (respectInlineDisables && current.line > 0) {
66092
66317
  const lines = getFileLines(current.filePath);
66093
66318
  if (lines) {
66094
- const ruleIdentifier = `${current.plugin}/${current.rule}`;
66095
66319
  const evaluation = evaluateSuppression(lines, current.line - 1, ruleIdentifier);
66096
66320
  if (evaluation.isSuppressed) return suppress(current, evaluation.isForeignDirective ? "foreign-inline" : "inline");
66097
66321
  if (evaluation.nearMissHint) current = {
@@ -66367,7 +66591,8 @@ const readSystemFacts$1 = () => ({
66367
66591
  /**
66368
66592
  * Auto lint-worker count: the smaller of the (cgroup-CPU-aware) core count and
66369
66593
  * the number of `PER_WORKER_MEM_BUDGET_BYTES` workers that fit in available
66370
- * memory, then clamped to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
66594
+ * memory, and the measured automatic parallel-efficiency ceiling, then clamped
66595
+ * to `[MIN, HARD_MAX]` by `resolveScanConcurrency`.
66371
66596
  *
66372
66597
  * `os.availableParallelism()` already respects cgroup CPU quotas, so the core
66373
66598
  * term needs no help. Available memory is `os.totalmem()` floored by the cgroup
@@ -66383,7 +66608,7 @@ const readSystemFacts$1 = () => ({
66383
66608
  const resolveAutoScanConcurrency = (facts = readSystemFacts$1()) => {
66384
66609
  const availableMemoryBytes = Math.min(facts.totalMemoryBytes, facts.cgroupMemoryLimitBytes ?? Number.POSITIVE_INFINITY);
66385
66610
  const memoryBoundedWorkers = Math.floor(availableMemoryBytes / PER_WORKER_MEM_BUDGET_BYTES);
66386
- return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers));
66611
+ return resolveScanConcurrency(Math.min(facts.availableCores, memoryBoundedWorkers, 10));
66387
66612
  };
66388
66613
  const resolveLintBatchOrdering = () => {
66389
66614
  return process.env["REACT_DOCTOR_LINT_BATCH_ORDERING"]?.trim().toLowerCase() === "arrival" ? "arrival" : "cost";
@@ -66396,24 +66621,22 @@ const resolveLintBatchOrdering = () => {
66396
66621
  */
66397
66622
  var OxlintSpawnTimeoutMs = class extends Reference("react-doctor/OxlintSpawnTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS", OXLINT_SPAWN_TIMEOUT_MS) }) {};
66398
66623
  /**
66399
- * Effect-side cap on the lint phase. The env var lets CI / eval runners
66400
- * raise the phase budget for slow large repos without recompiling.
66624
+ * Optional Effect-side cap on the lint phase. Unbounded unless explicitly
66625
+ * configured through the env var or a test layer.
66401
66626
  * Tests override via `Layer.succeed(LintPhaseTimeoutMs, ...)`.
66402
66627
  */
66403
- var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", LINT_PHASE_TIMEOUT_MS) }) {};
66628
+ var LintPhaseTimeoutMs = class extends Reference("react-doctor/LintPhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_LINT_PHASE_TIMEOUT_MS", null) }) {};
66404
66629
  /**
66405
- * Effect-side cap on the dead-code phase, sitting above the in-worker
66406
- * timeout as a runtime-independent backstop. The env var raises it for
66407
- * type-heavy projects; tests override via
66630
+ * Optional Effect-side cap on the dead-code phase. Unbounded unless explicitly
66631
+ * configured through the env var or a test layer. Tests override via
66408
66632
  * `Layer.succeed(DeadCodePhaseTimeoutMs, ...)`.
66409
66633
  */
66410
- var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", DEAD_CODE_PHASE_TIMEOUT_MS) }) {};
66634
+ var DeadCodePhaseTimeoutMs = class extends Reference("react-doctor/DeadCodePhaseTimeoutMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_DEAD_CODE_PHASE_TIMEOUT_MS", null) }) {};
66411
66635
  /**
66412
- * Overall scan deadline backstop, bounding everything the per-phase
66413
- * timeouts don't (wedged git / IO). The env var raises it for very
66414
- * large repos; tests override via `Layer.succeed(ScanDeadlineMs, ...)`.
66636
+ * Optional overall scan deadline. Unbounded unless explicitly configured
66637
+ * through the env var or a test layer.
66415
66638
  */
66416
- var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", SCAN_TOTAL_DEADLINE_MS) }) {};
66639
+ var ScanDeadlineMs = class extends Reference("react-doctor/ScanDeadlineMs", { defaultValue: () => readPositiveEnvMs("REACT_DOCTOR_SCAN_DEADLINE_MS", null) }) {};
66417
66640
  /**
66418
66641
  * Wall-clock budget for the supply-chain check when it runs on a background
66419
66642
  * fiber overlapping the lint pass. Reads from the env var on startup so the
@@ -66486,14 +66709,14 @@ var DeadCodeOverlap = class extends Reference("react-doctor/DeadCodeOverlap", {
66486
66709
  /**
66487
66710
  * How the full-scan lint pass plans its file batches. `"cost"` (the default)
66488
66711
  * builds size-balanced LPT batches (`planLintBatches`): the same mandatory
66489
- * batch count as greedy chunking (`ceil(files / 100)`), but every batch gets
66490
- * an even share of files AND bytes, so no 100-file chunk is a straggler while
66712
+ * batch count as greedy chunking, but every batch gets an even share of files
66713
+ * AND bytes, so no full chunk is a straggler while
66491
66714
  * the remainder-batch worker idles — and the heavy files are SPREAD across
66492
66715
  * batches, the precondition the old sort-desc-then-chunk-100 `cost` mode
66493
66716
  * lacked (it packed the heaviest files into one wave-1 straggler batch,
66494
66717
  * measurably regressing size-skewed repos, which is why it never earned the
66495
66718
  * default). `"arrival"` (`REACT_DOCTOR_LINT_BATCH_ORDERING=arrival`) is the
66496
- * rollback hatch to plain greedy 100-file chunking in discovery order. Tests
66719
+ * rollback hatch to plain greedy fixed-size chunking in discovery order. Tests
66497
66720
  * override via `Layer.succeed(LintBatchOrdering, ...)`. Diff / staged scans
66498
66721
  * never reach this — they pass user-scoped `includePaths` that skip discovery
66499
66722
  * and stay in arrival order; only the full-scan branch reads it.
@@ -66588,6 +66811,7 @@ const DEFAULT_SURFACE_EXCLUDED_TAGS = {
66588
66811
  score: ["design"],
66589
66812
  ciFailure: ["design"]
66590
66813
  };
66814
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66591
66815
  const VALID_RULE_SEVERITIES = [
66592
66816
  "error",
66593
66817
  "warn",
@@ -66630,7 +66854,6 @@ const SURFACE_CONTROL_FIELD_NAMES = [
66630
66854
  ];
66631
66855
  const isIncludedFileContext = (value) => VALID_INCLUDED_FILE_CONTEXTS.some((fileContext) => fileContext === value);
66632
66856
  const SEVERITY_FIELD_NAMES = ["rules", "categories"];
66633
- const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66634
66857
  const formatType = (value) => typeof value === "string" ? `"${value}"` : typeof value;
66635
66858
  const isRuleSeverity = (value) => typeof value === "string" && VALID_RULE_SEVERITIES.includes(value);
66636
66859
  const coerceMaybeBooleanString = (fieldName, value) => {
@@ -66658,7 +66881,7 @@ const validateStringArrayField = (fieldName, value) => {
66658
66881
  });
66659
66882
  };
66660
66883
  const validateSurfaceControls = (surface, rawControls) => {
66661
- if (!isPlainObject$1(rawControls)) {
66884
+ if (!isRecord(rawControls)) {
66662
66885
  warnConfigIssue(`config field "surfaces.${surface}" must be an object (got ${typeof rawControls}); ignoring this surface.`);
66663
66886
  return;
66664
66887
  }
@@ -66682,7 +66905,7 @@ const validateSurfaceControls = (surface, rawControls) => {
66682
66905
  return validatedSurfaceControls;
66683
66906
  };
66684
66907
  const validateSurfacesField = (rawSurfaces) => {
66685
- if (!isPlainObject$1(rawSurfaces)) {
66908
+ if (!isRecord(rawSurfaces)) {
66686
66909
  warnConfigIssue(`config field "surfaces" must be an object (got ${typeof rawSurfaces}); ignoring this field.`);
66687
66910
  return;
66688
66911
  }
@@ -66698,7 +66921,7 @@ const validateSurfacesField = (rawSurfaces) => {
66698
66921
  return validated;
66699
66922
  };
66700
66923
  const validateSeverityMap = (fieldName, rawMap, keysAreCategories = false) => {
66701
- if (!isPlainObject$1(rawMap)) {
66924
+ if (!isRecord(rawMap)) {
66702
66925
  warnConfigIssue(`config field "${fieldName}" must be an object (got ${typeof rawMap}); ignoring this field.`);
66703
66926
  return;
66704
66927
  }
@@ -67031,6 +67254,10 @@ const dedupeRelatedDiagnostics = (diagnostics) => {
67031
67254
  }
67032
67255
  return uniqueDiagnostics;
67033
67256
  };
67257
+ const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
67258
+ const relativePath = Path.relative(parentAbsolutePath, childAbsolutePath);
67259
+ return Boolean(relativePath) && !relativePath.startsWith("..") && !Path.isAbsolute(relativePath);
67260
+ };
67034
67261
  const compareStrings = (left, right) => left < right ? -1 : left > right ? 1 : 0;
67035
67262
  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));
67036
67263
  const getDirectDependencyNames = (packageJson) => new Set([...Object.keys(packageJson.dependencies ?? {}), ...Object.keys(packageJson.devDependencies ?? {})]);
@@ -68607,7 +68834,7 @@ const readScannedFile = (candidate) => {
68607
68834
  return null;
68608
68835
  }
68609
68836
  };
68610
- function* collectSecurityScanFiles(rootDirectory) {
68837
+ function* collectSecurityScanFiles(rootDirectory, excludedDirectories = /* @__PURE__ */ new Set()) {
68611
68838
  const priorityCandidates = [];
68612
68839
  const artifactCandidates = [];
68613
68840
  const otherCandidates = [];
@@ -68622,7 +68849,7 @@ function* collectSecurityScanFiles(rootDirectory) {
68622
68849
  for (const entry of readDirectoryEntries(current.absolutePath)) {
68623
68850
  const absolutePath = Path.join(current.absolutePath, entry.name);
68624
68851
  if (entry.isDirectory()) {
68625
- if (!SKIPPED_DIRECTORY_NAMES.has(entry.name)) stack.push({
68852
+ if (!SKIPPED_DIRECTORY_NAMES.has(entry.name) && !excludedDirectories.has(Path.resolve(absolutePath))) stack.push({
68626
68853
  absolutePath,
68627
68854
  depth: current.depth + 1
68628
68855
  });
@@ -68803,10 +69030,8 @@ const shouldEnableRule = (requires, tags, capabilities, ignoredTags, disabledWhe
68803
69030
  }
68804
69031
  return true;
68805
69032
  };
69033
+ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
68806
69034
  const shouldEnableRuleByDefaultStatus = (options) => options.defaultEnabled !== false || options.hasExplicitOverride === true || options.includeTagDefaults && options.hasIncludedTags;
68807
- const yieldToEventLoop = () => new Promise((resolve) => {
68808
- setImmediate(resolve);
68809
- });
68810
69035
  const createSecurityScanSession = (rootDirectory, options) => {
68811
69036
  const capabilities = options.project ? getCapabilities(options.project) : /* @__PURE__ */ new Set();
68812
69037
  const ignoredTags = options.ignoredTags ?? /* @__PURE__ */ new Set();
@@ -68859,19 +69084,35 @@ const createSecurityScanSession = (rootDirectory, options) => {
68859
69084
  };
68860
69085
  };
68861
69086
  const checkSecurityScanCooperative = async (rootDirectory, options = {}) => {
69087
+ const throwIfScanAborted = () => {
69088
+ options.signal?.throwIfAborted();
69089
+ };
69090
+ const didReachDeadline = () => {
69091
+ const hasDeadlineElapsed = options.deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(options.deadlineEpochMs) === 0;
69092
+ if (hasDeadlineElapsed) options.onDeadlineExceeded?.();
69093
+ return hasDeadlineElapsed;
69094
+ };
69095
+ throwIfScanAborted();
68862
69096
  const session = createSecurityScanSession(rootDirectory, options);
68863
69097
  if (session === null) return [];
69098
+ if (didReachDeadline()) return [];
68864
69099
  let sliceStartedAt = performance.now();
68865
- for (const file of collectSecurityScanFiles(rootDirectory)) {
69100
+ for (const file of collectSecurityScanFiles(rootDirectory, options.excludedDirectories)) {
69101
+ throwIfScanAborted();
69102
+ if (didReachDeadline()) return session.diagnostics;
68866
69103
  if (file === null) {
68867
69104
  if (performance.now() - sliceStartedAt >= 12) {
68868
69105
  await yieldToEventLoop();
69106
+ throwIfScanAborted();
69107
+ if (didReachDeadline()) return session.diagnostics;
68869
69108
  sliceStartedAt = performance.now();
68870
69109
  }
68871
69110
  continue;
68872
69111
  }
68873
69112
  for (const _ruleStep of session.scanFileByRule(file)) if (performance.now() - sliceStartedAt >= 12) {
68874
69113
  await yieldToEventLoop();
69114
+ throwIfScanAborted();
69115
+ if (didReachDeadline()) return session.diagnostics;
68875
69116
  sliceStartedAt = performance.now();
68876
69117
  }
68877
69118
  }
@@ -69034,25 +69275,18 @@ const collectIgnorePatterns = (rootDirectory) => {
69034
69275
  cachedPatternsByRoot.set(rootDirectory, patterns);
69035
69276
  return patterns;
69036
69277
  };
69037
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
69038
- const KNIP_JSON_FILENAME = "knip.json";
69039
- const readJsonFileSafe = (filePath) => {
69040
- let rawContents;
69041
- try {
69042
- rawContents = fs.readFileSync(filePath, "utf-8");
69043
- } catch {
69044
- return null;
69045
- }
69278
+ const failOpenReadJson = (filePath, fallback) => {
69046
69279
  try {
69047
- return JSON.parse(rawContents);
69280
+ return JSON.parse(NFS.readFileSync(filePath, "utf8"));
69048
69281
  } catch {
69049
- return null;
69282
+ return fallback;
69050
69283
  }
69051
69284
  };
69285
+ const KNIP_JSON_FILENAME = "knip.json";
69052
69286
  const readKnipConfig = (rootDirectory) => {
69053
- const knipJson = readJsonFileSafe(path.join(rootDirectory, KNIP_JSON_FILENAME));
69287
+ const knipJson = failOpenReadJson(path.join(rootDirectory, KNIP_JSON_FILENAME), null);
69054
69288
  if (isRecord(knipJson)) return knipJson;
69055
- const packageJson = readJsonFileSafe(path.join(rootDirectory, "package.json"));
69289
+ const packageJson = failOpenReadJson(path.join(rootDirectory, "package.json"), null);
69056
69290
  const packageKnipConfig = isRecord(packageJson) ? packageJson.knip : null;
69057
69291
  return isRecord(packageKnipConfig) ? packageKnipConfig : null;
69058
69292
  };
@@ -69100,13 +69334,6 @@ const atomicWriteJson = (filePath, value) => {
69100
69334
  return;
69101
69335
  }
69102
69336
  };
69103
- const failOpenReadJson = (filePath, fallback) => {
69104
- try {
69105
- return JSON.parse(NFS.readFileSync(filePath, "utf8"));
69106
- } catch {
69107
- return fallback;
69108
- }
69109
- };
69110
69337
  const hashFileContents = (filePath) => {
69111
69338
  try {
69112
69339
  return crypto.createHash("sha1").update(NFS.readFileSync(filePath)).digest("hex");
@@ -69785,7 +70012,6 @@ const isDiagnosticOnSurface = (diagnostic, surface, config) => {
69785
70012
  return true;
69786
70013
  };
69787
70014
  const filterDiagnosticsForSurface = (diagnostics, surface, config) => diagnostics.filter((diagnostic) => isDiagnosticOnSurface(diagnostic, surface, config));
69788
- const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs - Date.now(), 0);
69789
70015
  /**
69790
70016
  * Budget for the dead-code phase, scaled to the work. deslop's graph build is
69791
70017
  * CPU-bound and roughly linear in file count, so a fixed 120s cap is too tight
@@ -69793,25 +70019,28 @@ const remainingDeadlineBudgetMs = (deadlineEpochMs) => Math.max(deadlineEpochMs
69793
70019
  * tipped over by any concurrent load — silently dropping every dead-code
69794
70020
  * finding. Scaling the budget with file count (and inversely with the core
69795
70021
  * share when overlapped) lets the pass complete, while the ceiling still
69796
- * reclaims a genuinely wedged worker. Returns the in-worker SIGKILL deadline
69797
- * and the Effect-side phase backstop that sits a margin above it.
70022
+ * reclaims a genuinely wedged worker.
69798
70023
  */
69799
70024
  const resolveDeadCodeTimeout = (input) => {
69800
70025
  const coreShareFactor = Math.max(1, input.fullConcurrency / Math.max(1, input.deadCodeConcurrency));
69801
- const workerTimeoutMs = Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor)));
69802
- return {
69803
- workerTimeoutMs,
69804
- phaseTimeoutMs: workerTimeoutMs + DEAD_CODE_PHASE_TIMEOUT_OVER_WORKER_MS
69805
- };
70026
+ return { workerTimeoutMs: Math.min(DEAD_CODE_TIMEOUT_CEILING_MS, Math.max(DEAD_CODE_WORKER_TIMEOUT_MS, Math.ceil(input.sourceFileCount * 30 * coreShareFactor))) };
69806
70027
  };
69807
- const resolveLintIncludePaths = (rootDirectory, userConfig) => {
70028
+ const resolveLintIncludePaths = (rootDirectory, userConfig, sourceFiles) => {
69808
70029
  if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) return;
69809
70030
  const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
69810
- return listSourceFiles(rootDirectory).filter((filePath) => {
70031
+ return (sourceFiles ?? listSourceFiles(rootDirectory)).filter((filePath) => {
69811
70032
  if (!isLintableSourceFile(filePath)) return false;
69812
70033
  return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
69813
70034
  });
69814
70035
  };
70036
+ const filterPathsOutsideDirectories = (input) => {
70037
+ 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}/`)));
70038
+ if (excludedPrefixes.length === 0) return [...input.relativePaths];
70039
+ return input.relativePaths.filter((relativePath) => {
70040
+ const normalizedRelativePath = relativePath.replaceAll("\\", "/").replace(/^\.\//, "");
70041
+ return !excludedPrefixes.some((excludedPrefix) => normalizedRelativePath === excludedPrefix || normalizedRelativePath.startsWith(`${excludedPrefix}/`));
70042
+ });
70043
+ };
69815
70044
  var Config = class Config extends Service()("react-doctor/Config") {
69816
70045
  static layerNode = effect(Config, gen(function* () {
69817
70046
  const cache = yield* make$3({
@@ -69873,6 +70102,7 @@ var Files = class Files extends Service()("react-doctor/Files") {
69873
70102
  static layerNode = succeed$3(Files, Files.of({
69874
70103
  readLines: (input) => sync(() => createNodeReadFileLinesSync(input.rootDirectory)(input.filePath)),
69875
70104
  listSourceFiles: (rootDirectory) => sync(() => listSourceFiles(rootDirectory)),
70105
+ listSourceFilesCooperative: (input) => promise(() => listSourceFilesCooperative(input.rootDirectory, input.signal)),
69876
70106
  isFile: (filePath) => sync(() => isFile(filePath)),
69877
70107
  isDirectory: (filePath) => sync(() => isDirectory(filePath))
69878
70108
  }));
@@ -69899,6 +70129,16 @@ var Files = class Files extends Service()("react-doctor/Files") {
69899
70129
  }
69900
70130
  return files;
69901
70131
  }),
70132
+ listSourceFilesCooperative: (input) => sync(() => {
70133
+ const rootDirectory = input.rootDirectory;
70134
+ const prefix = rootDirectory.endsWith("/") ? rootDirectory : `${rootDirectory}/`;
70135
+ const files = [];
70136
+ for (const absolute of tree.keys()) {
70137
+ if (!absolute.startsWith(prefix)) continue;
70138
+ files.push(absolute.slice(prefix.length));
70139
+ }
70140
+ return files;
70141
+ }),
69902
70142
  isFile: (filePath) => sync(() => tree.has(filePath)),
69903
70143
  isDirectory: (filePath) => sync(() => {
69904
70144
  const prefix = filePath.endsWith("/") ? filePath : `${filePath}/`;
@@ -70474,7 +70714,7 @@ const batchIncludePaths = (baseArgs, includePaths) => {
70474
70714
  for (const filePath of includePaths) {
70475
70715
  const entryLength = filePath.length + 1;
70476
70716
  const exceedsArgLength = currentBatch.length > 0 && currentBatchLength + entryLength > 24e3;
70477
- const exceedsFileCount = currentBatch.length >= 100;
70717
+ const exceedsFileCount = currentBatch.length >= 200;
70478
70718
  if (exceedsArgLength || exceedsFileCount) {
70479
70719
  batches.push(currentBatch);
70480
70720
  currentBatch = [];
@@ -70821,7 +71061,7 @@ const buildUserPluginRules = (userPlugin, severityControls) => {
70821
71061
  }
70822
71062
  return enabled;
70823
71063
  };
70824
- 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 }) => {
71064
+ 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 }) => {
70825
71065
  const hasIncludedTags = includedTags.size > 0;
70826
71066
  const reactHooksJsPlugin = disableReactHooksJsPlugin || ruleSelection === "sidecar" || hasIncludedTags ? null : resolveReactHooksJsPlugin(project.hasReactCompiler || project.hasReactCompilerLintPlugin === true, customRulesOnly);
70827
71067
  const reactCompilerRules = reactHooksJsPlugin ? applyRuleSeverityControls(filterRulesToAvailable(REACT_COMPILER_RULES, "react-hooks-js", reactHooksJsPlugin.availableRuleNames), severityControls) : {};
@@ -70885,7 +71125,8 @@ const createOxlintConfig = ({ pluginPath, project, customRulesOnly = false, exte
70885
71125
  }))
70886
71126
  } : {},
70887
71127
  ...project.shopifyFlashListMajorVersion !== null ? { shopifyFlashListMajorVersion: project.shopifyFlashListMajorVersion } : {},
70888
- ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {}
71128
+ ...serverAuthFunctionNames && serverAuthFunctionNames.length > 0 ? { serverAuthFunctionNames: [...serverAuthFunctionNames] } : {},
71129
+ ...projectIndexModuleSources !== void 0 ? { projectIndexModuleSources: [...projectIndexModuleSources] } : {}
70889
71130
  } },
70890
71131
  rules: {
70891
71132
  ...reactCompilerRules,
@@ -71134,11 +71375,17 @@ const collectUnpluginAutoImportGlobalScopes = ({ rootDirectory, candidateFiles }
71134
71375
  return scopes.some((scope) => scope.names.length > 0) ? scopes : [];
71135
71376
  };
71136
71377
  const validateDiagnostic$1 = decodeUnknownSync(Diagnostic);
71378
+ const decodeDiagnostic = (raw) => {
71379
+ const diagnostic = validateDiagnostic$1(raw);
71380
+ return {
71381
+ ...diagnostic,
71382
+ relatedLocations: diagnostic.relatedLocations?.map((location) => ({ ...location }))
71383
+ };
71384
+ };
71137
71385
  const decodeFileDiagnostics = (raw) => {
71138
71386
  if (!Array.isArray(raw)) return null;
71139
71387
  try {
71140
- for (const entry of raw) validateDiagnostic$1(entry);
71141
- return raw;
71388
+ return raw.map((entry) => decodeDiagnostic(entry));
71142
71389
  } catch {
71143
71390
  return null;
71144
71391
  }
@@ -72370,6 +72617,12 @@ const buildProfiledNodeArguments = (input) => [
72370
72617
  ...input.heapProfileDirectory ? ["--heap-prof", `--heap-prof-dir=${input.heapProfileDirectory}`] : [],
72371
72618
  ...input.argumentsList
72372
72619
  ];
72620
+ const lowerChildProcessPriority = (processId) => {
72621
+ if (processId === void 0) return;
72622
+ try {
72623
+ OS.setPriority(processId, OS.constants.priority.PRIORITY_BELOW_NORMAL);
72624
+ } catch {}
72625
+ };
72373
72626
  const SANITIZED_ENV = buildOxlintChildEnv(process.env);
72374
72627
  /**
72375
72628
  * Spawn one oxlint subprocess with hard ceilings on wall time and
@@ -72408,6 +72661,7 @@ const spawnOxlint = (args, rootDirectory, nodeBinaryPath, spawnTimeoutMs = OXLIN
72408
72661
  "pipe"
72409
72662
  ]
72410
72663
  });
72664
+ lowerChildProcessPriority(child.pid);
72411
72665
  const onAbort = () => {
72412
72666
  child.kill("SIGKILL");
72413
72667
  reject(new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) }));
@@ -72530,7 +72784,7 @@ const isParallelismRelatedSpawnError = (error) => {
72530
72784
  * loop with a slimmer config in that case.
72531
72785
  */
72532
72786
  const spawnLintBatches = async (input) => {
72533
- const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 8, signal, deadlineEpochMs } = input;
72787
+ const { baseArgs, fileBatches, rootDirectory, nodeBinaryPath, project, sourcePathByLintPath, sourceMapByLintPath, onPartialFailure, onFileProgress, spawnTimeoutMs, outputMaxBytes, splitTotalBudgetMs = OXLINT_SPLIT_TOTAL_BUDGET_MS, splitMaxDepth = 9, signal, deadlineEpochMs } = input;
72534
72788
  const resolveSourcePath = (filePath) => {
72535
72789
  const absoluteFilePath = Path.isAbsolute(filePath) ? filePath : Path.resolve(rootDirectory, filePath);
72536
72790
  return sourcePathByLintPath?.get(Path.normalize(absoluteFilePath)) ?? filePath;
@@ -72538,6 +72792,12 @@ const spawnLintBatches = async (input) => {
72538
72792
  const requestedConcurrency = resolveScanConcurrency(input.concurrency ?? 1);
72539
72793
  let rescueDeadlineEpochMs;
72540
72794
  const isPastDeadline = () => deadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(deadlineEpochMs) === 0 || rescueDeadlineEpochMs !== void 0 && remainingDeadlineBudgetMs(rescueDeadlineEpochMs) === 0;
72795
+ const resolveSpawnTimeoutMs = () => {
72796
+ const deadlineBudgets = [deadlineEpochMs, rescueDeadlineEpochMs].filter((deadline) => deadline !== void 0).map(remainingDeadlineBudgetMs);
72797
+ if (deadlineBudgets.length === 0) return spawnTimeoutMs;
72798
+ const remainingDeadlineMs = Math.min(...deadlineBudgets);
72799
+ return spawnTimeoutMs === void 0 ? remainingDeadlineMs : Math.min(spawnTimeoutMs, remainingDeadlineMs);
72800
+ };
72541
72801
  const runBatchPass = async (concurrency, passBatches, passOnFileProgress) => {
72542
72802
  const totalFileCount = passBatches.reduce((sum, batch) => sum + batch.length, 0);
72543
72803
  const allDiagnostics = [];
@@ -72555,12 +72815,13 @@ const spawnLintBatches = async (input) => {
72555
72815
  const batchArgs = [...baseArgs, ...batch];
72556
72816
  try {
72557
72817
  const spawnBatch = () => {
72558
- if (isPastDeadline()) {
72818
+ const effectiveSpawnTimeoutMs = resolveSpawnTimeoutMs();
72819
+ if (effectiveSpawnTimeoutMs === 0) {
72559
72820
  deadlineSkippedFiles.push(...batch);
72560
72821
  batchState.deadlineSkippedFileCount += batch.length;
72561
72822
  return Promise.resolve(null);
72562
72823
  }
72563
- return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, spawnTimeoutMs, outputMaxBytes, signal, () => {
72824
+ return spawnOxlint(batchArgs, rootDirectory, nodeBinaryPath, effectiveSpawnTimeoutMs, outputMaxBytes, signal, () => {
72564
72825
  if (batchState.didStart) return;
72565
72826
  batchState.didStart = true;
72566
72827
  startedFileCount += batchState.initialFileCount;
@@ -72715,11 +72976,36 @@ const validateRuleRegistration = () => {
72715
72976
  ].filter((entry) => entry !== null).join("; ");
72716
72977
  console.warn(`[react-doctor] rule-registration drift: ${detail}`);
72717
72978
  };
72979
+ const PROJECT_INDEX_MODULE_SOURCES = [
72980
+ "next/og",
72981
+ "@vercel/og",
72982
+ "satori",
72983
+ "remotion"
72984
+ ];
72985
+ const collectProjectIndexModuleSources = async (rootDirectory, candidateFiles) => {
72986
+ const foundModuleSources = /* @__PURE__ */ new Set();
72987
+ let sliceStartedAt = performance.now();
72988
+ for (const candidateFile of candidateFiles) {
72989
+ let sourceBuffer;
72990
+ try {
72991
+ sourceBuffer = NFS.readFileSync(Path.isAbsolute(candidateFile) ? candidateFile : Path.resolve(rootDirectory, candidateFile));
72992
+ } catch {
72993
+ continue;
72994
+ }
72995
+ for (const moduleSource of PROJECT_INDEX_MODULE_SOURCES) if (sourceBuffer.includes(`"${moduleSource}"`) || sourceBuffer.includes(`'${moduleSource}'`)) foundModuleSources.add(moduleSource);
72996
+ if (foundModuleSources.size === PROJECT_INDEX_MODULE_SOURCES.length) break;
72997
+ if (performance.now() - sliceStartedAt >= 12) {
72998
+ await yieldToEventLoop();
72999
+ sliceStartedAt = performance.now();
73000
+ }
73001
+ }
73002
+ return [...foundModuleSources].sort();
73003
+ };
72718
73004
  /**
72719
73005
  * Balanced LPT (longest-processing-time-first) lint batch planner. Where
72720
- * `batchIncludePaths` greedily fills 100-file batches and leaves a small
72721
- * remainder batch (617 files → 6×100 + 17, so the parallel pool's wall clock
72722
- * is set by whichever 100-file chunk happened to collect the most work while
73006
+ * `batchIncludePaths` greedily fills fixed-size batches and leaves a small
73007
+ * remainder batch, so the parallel pool's wall clock is set by whichever
73008
+ * full chunk happened to collect the most work while
72723
73009
  * the remainder's worker idles), this planner keeps the SAME mandatory batch
72724
73010
  * count — `ceil(files / OXLINT_MAX_FILES_PER_BATCH)`, the JS-plugin
72725
73011
  * perf-cliff / native-binding SIGABRT guard — and assigns files largest-first,
@@ -72748,7 +73034,7 @@ const validateRuleRegistration = () => {
72748
73034
  */
72749
73035
  const planLintBatches = ({ baseArgs, files, sizeByFile }) => {
72750
73036
  if (files.length === 0) return [];
72751
- const batchCount = Math.ceil(files.length / 100);
73037
+ const batchCount = Math.ceil(files.length / 200);
72752
73038
  const maxFilesPerBatch = Math.ceil(files.length / batchCount);
72753
73039
  const baseArgsLengthChars = estimateArgsLength(baseArgs);
72754
73040
  const descendingSizeIndexes = [...files.keys()].sort((leftIndex, rightIndex) => (sizeByFile.get(files[rightIndex]) ?? 0) - (sizeByFile.get(files[leftIndex]) ?? 0));
@@ -73277,6 +73563,7 @@ const runOxlint = async (options) => {
73277
73563
  await yieldToEventLoop();
73278
73564
  const sizedScanFiles = includePaths === void 0 ? listSourceFilesWithSize(rootDirectory) : null;
73279
73565
  const candidateFiles = includePaths !== void 0 ? includePaths : (sizedScanFiles ?? []).map((entry) => entry.path);
73566
+ const projectIndexModuleSources = includePaths === void 0 && options.deadlineEpochMs === void 0 ? await collectProjectIndexModuleSources(rootDirectory, candidateFiles) : void 0;
73280
73567
  unpluginAutoImportGlobalScopes = collectUnpluginAutoImportGlobalScopes({
73281
73568
  rootDirectory,
73282
73569
  candidateFiles
@@ -73299,6 +73586,7 @@ const runOxlint = async (options) => {
73299
73586
  runtimeGlobals,
73300
73587
  unpluginAutoImportGlobalScopes,
73301
73588
  serverAuthFunctionNames,
73589
+ projectIndexModuleSources,
73302
73590
  severityControls,
73303
73591
  userPlugins,
73304
73592
  disableReactHooksJsPlugin: overrides.disableReactHooksJsPlugin,
@@ -73766,10 +74054,10 @@ const translateProjectInfoError = (cause, directory) => {
73766
74054
  }) });
73767
74055
  };
73768
74056
  var Project = class Project extends Service()("react-doctor/Project") {
73769
- static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (directory) {
74057
+ static layerNode = succeed$3(Project, Project.of({ discover: fn("Project.discover")(function* (input) {
73770
74058
  return yield* try_({
73771
- try: () => discoverProject(directory),
73772
- catch: (cause) => translateProjectInfoError(cause, directory)
74059
+ try: () => discoverProject(input.directory, { sourceFileCount: input.sourceFileCount }),
74060
+ catch: (cause) => translateProjectInfoError(cause, input.directory)
73773
74061
  });
73774
74062
  }) }));
73775
74063
  static layerOf = (projectInfo) => succeed$3(Project, Project.of({ discover: () => succeed$2(projectInfo) }));
@@ -74354,9 +74642,10 @@ const checkSupplyChain = (input) => gen(function* () {
74354
74642
  * outage.
74355
74643
  */
74356
74644
  var SupplyChain = class SupplyChain extends Service()("react-doctor/SupplyChain") {
74357
- static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (totalTimeoutMs) => checkSupplyChain({
74358
- ...input,
74359
- totalTimeoutMs
74645
+ static layerNode = succeed$3(SupplyChain, SupplyChain.of({ run: (input) => unwrap(flatMap$2(SupplyChainOverlapTimeoutMs, (configuredTimeoutMs) => checkSupplyChain({
74646
+ rootDirectory: input.rootDirectory,
74647
+ userConfig: input.userConfig,
74648
+ totalTimeoutMs: input.timeoutMs ?? configuredTimeoutMs
74360
74649
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), withSpan("SupplyChain.run"))) }));
74361
74650
  static layerOf = (diagnostics) => succeed$3(SupplyChain, SupplyChain.of({ run: () => fromIterable$1(diagnostics) }));
74362
74651
  };
@@ -74391,10 +74680,6 @@ const NO_HOOKS = {
74391
74680
  beforeLint: () => void_,
74392
74681
  afterLint: () => void_
74393
74682
  };
74394
- const filterMapNullable = (transform) => fromPredicateOption((value) => {
74395
- const result = transform(value);
74396
- return result === null ? none() : some(result);
74397
- });
74398
74683
  const fileReader = (filesService, rootDirectory) => (filePath) => {
74399
74684
  const lines = runSync(filesService.readLines({
74400
74685
  filePath,
@@ -74418,7 +74703,7 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
74418
74703
  * The GitHub viewer-permission lookup is forked onto a background
74419
74704
  * fiber here and joined late (it feeds score metadata, not
74420
74705
  * diagnostics).
74421
- * 2. beforeLint hook (e.g. CLI renders the project-detection block)
74706
+ * 2. beforeLint hook (e.g. CLI records project telemetry)
74422
74707
  * 3. environment checks (reduced-motion + pnpm hardening +
74423
74708
  * expo/react-native), collected synchronously. The heavier
74424
74709
  * content-regex security scan is forked instead (like supply-chain
@@ -74431,13 +74716,11 @@ const formatLintFailText = (reasonTag, nodeVersion) => {
74431
74716
  * `SupplyChainOverlapTimeoutMs` (measured from fork) so a hung
74432
74717
  * socket can't drag out its join; on timeout it fails open to no
74433
74718
  * diagnostics — the same outcome class as a Socket outage.
74434
- * 5. Linter.run runs; DeadCode.run runs concurrently (forked child
74435
- * fiber) ONLY when the memory gate has headroom to run the 8 GB
74436
- * dead-code child alongside the oxlint workers — or when overlap is
74437
- * forced via REACT_DOCTOR_DEAD_CODE_OVERLAP. Otherwise dead-code
74438
- * runs sequentially after lint, exactly as it did pre-overlap. The
74439
- * fiber is joined (or interrupted, SIGKILLing its worker, on lint
74440
- * failure) before diagnostics are concatenated. The afterLint hook
74719
+ * 5. Linter.run runs; DeadCode.run forks only when
74720
+ * REACT_DOCTOR_DEAD_CODE_OVERLAP=on. The default "auto" and explicit
74721
+ * "off" paths stay sequential. The fiber is joined (or interrupted,
74722
+ * SIGKILLing its worker, on lint failure) before diagnostics are
74723
+ * concatenated. The afterLint hook
74441
74724
  * fires between lint and dead-code. Progress spinner labels AND the
74442
74725
  * final diagnostic / score order stay independent of execution
74443
74726
  * order, so terminal output is identical either way; supply-chain
@@ -74472,8 +74755,28 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74472
74755
  const partialFailuresRef = yield* LintPartialFailures;
74473
74756
  const resolvedConfig = yield* configService.resolve(input.directory);
74474
74757
  const scanDirectory = resolvedConfig.resolvedDirectory;
74475
- const project = yield* projectService.discover(scanDirectory);
74758
+ const excludedProjectDirectories = (input.excludedProjectDirectories ?? []).map((excludedDirectory) => Path.resolve(excludedDirectory)).filter((excludedDirectory) => isPathInsideDirectory(excludedDirectory, scanDirectory));
74759
+ const excludedProjectRelativePaths = excludedProjectDirectories.map((excludedDirectory) => Path.relative(scanDirectory, excludedDirectory).replaceAll("\\", "/"));
74760
+ const isExcludedProjectDiagnostic = (diagnostic) => {
74761
+ const relativeFilePath = toNormalizedRelativePath(diagnostic.filePath, scanDirectory);
74762
+ return excludedProjectRelativePaths.some((excludedRelativePath) => relativeFilePath === excludedRelativePath || relativeFilePath.startsWith(`${excludedRelativePath}/`));
74763
+ };
74764
+ const precomputedSourceFiles = input.suppressScanSummary === true || excludedProjectDirectories.length > 0 ? yield* filesService.listSourceFilesCooperative({
74765
+ rootDirectory: scanDirectory,
74766
+ signal: input.signal
74767
+ }) : null;
74768
+ const includedPrecomputedSourceFiles = precomputedSourceFiles !== null && excludedProjectDirectories.length > 0 ? filterPathsOutsideDirectories({
74769
+ rootDirectory: scanDirectory,
74770
+ relativePaths: precomputedSourceFiles,
74771
+ excludedDirectories: excludedProjectDirectories
74772
+ }) : precomputedSourceFiles;
74773
+ const sourceFileCount = excludedProjectDirectories.length > 0 ? includedPrecomputedSourceFiles?.length : input.precomputedSourceFileCount ?? includedPrecomputedSourceFiles?.length;
74774
+ const project = yield* projectService.discover({
74775
+ directory: scanDirectory,
74776
+ sourceFileCount
74777
+ });
74476
74778
  if (!isAnalyzableProject(project)) return yield* new ReactDoctorError({ reason: new NoReactDependency({ directory: scanDirectory }) });
74779
+ const deadCodeSourceFileCount = input.retainExcludedProjectDeadCodeDiagnostics === true && precomputedSourceFiles !== null ? precomputedSourceFiles.length : project.sourceFileCount;
74477
74780
  const [repo, sha, defaultBranch] = yield* all([
74478
74781
  gitService.githubRepo(scanDirectory).pipe(orElseSucceed(() => null)),
74479
74782
  gitService.headSha(scanDirectory).pipe(orElseSucceed(() => null)),
@@ -74484,11 +74787,16 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74484
74787
  directory: scanDirectory,
74485
74788
  repo
74486
74789
  }).pipe(orElseSucceed(() => null)) : succeed$2(null));
74487
- const lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config);
74488
- const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
74790
+ let lintIncludePaths = (input.skipExplicitIncludePathFilter ? input.includePaths.length > 0 ? [...input.includePaths] : void 0 : computeExplicitLintIncludePaths([...input.includePaths])) ?? resolveLintIncludePaths(scanDirectory, resolvedConfig.config, includedPrecomputedSourceFiles ?? void 0);
74791
+ if (excludedProjectDirectories.length > 0) lintIncludePaths = filterPathsOutsideDirectories({
74792
+ rootDirectory: scanDirectory,
74793
+ relativePaths: lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory)),
74794
+ excludedDirectories: excludedProjectDirectories
74795
+ });
74796
+ const fallbackScannedFilePaths = input.suppressScanSummary ? (lintIncludePaths ?? includedPrecomputedSourceFiles ?? (yield* filesService.listSourceFiles(scanDirectory))).map((relativePath) => Path.resolve(scanDirectory, relativePath)) : [];
74489
74797
  const beforeLint = hooks.beforeLint ?? NO_HOOKS.beforeLint;
74490
74798
  const afterLint = hooks.afterLint ?? NO_HOOKS.afterLint;
74491
- yield* beforeLint(project, lintIncludePaths ?? void 0);
74799
+ yield* beforeLint(project);
74492
74800
  const isDiffMode = input.includePaths.length > 0;
74493
74801
  const showWarnings = input.warnings ?? resolvedConfig.config?.warnings ?? true;
74494
74802
  const transform = buildDiagnosticPipeline({
@@ -74498,7 +74806,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74498
74806
  respectInlineDisables: input.respectInlineDisables,
74499
74807
  showWarnings
74500
74808
  });
74501
- const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(filterMapNullable(transform.apply)));
74809
+ const filterPerElementPipeline = (rawStream) => rawStream.pipe(filterMap(fromPredicateOption((diagnostic) => {
74810
+ const filteredDiagnostic = transform.apply(diagnostic);
74811
+ return filteredDiagnostic === null ? none() : some(filteredDiagnostic);
74812
+ })));
74502
74813
  const applyPerElementPipeline = (rawStream) => filterPerElementPipeline(rawStream).pipe(tap((diagnostic) => reporterService.emit(diagnostic)));
74503
74814
  const envCollected = yield* runCollect(applyPerElementPipeline(fromIterable$1(isDiffMode ? [] : [
74504
74815
  ...checkReducedMotion(scanDirectory),
@@ -74508,17 +74819,31 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74508
74819
  ...checkReactNativeProject(scanDirectory, project)
74509
74820
  ])));
74510
74821
  const securityScanFailedRef = yield* make$13(false);
74511
- const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise(() => checkSecurityScanCooperative(scanDirectory, {
74822
+ let didSecurityScanReachDeadline = false;
74823
+ const securityScanFiber = yield* forkChild(runCollect(applyPerElementPipeline(isDiffMode ? empty$4 : unwrap(tryPromise((signal) => checkSecurityScanCooperative(scanDirectory, {
74512
74824
  project,
74513
74825
  ignoredTags: input.ignoredTags,
74514
74826
  includedTags: input.includedTags,
74515
- includeTagDefaults: input.includeTagDefaults
74827
+ includeTagDefaults: input.includeTagDefaults,
74828
+ excludedDirectories: new Set(excludedProjectDirectories),
74829
+ deadlineEpochMs: input.deadlineEpochMs,
74830
+ signal,
74831
+ onDeadlineExceeded: () => {
74832
+ didSecurityScanReachDeadline = true;
74833
+ }
74516
74834
  })).pipe(map$3((diagnostics) => fromIterable$1(diagnostics)), catch_$1(() => set(securityScanFailedRef, true).pipe(as(empty$4))))))).pipe(withSpan("SecurityScan.run")));
74835
+ const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
74836
+ const capOptionalToDeadline = (phaseTimeoutMs) => {
74837
+ if (input.deadlineEpochMs === void 0) return phaseTimeoutMs;
74838
+ const remainingBudgetMs = remainingDeadlineBudgetMs(input.deadlineEpochMs);
74839
+ return phaseTimeoutMs === null ? remainingBudgetMs : Math.min(phaseTimeoutMs, remainingBudgetMs);
74840
+ };
74517
74841
  const shouldRunSupplyChain = !isDiffMode || (input.supplyChainManifestChanged ?? false);
74518
- const supplyChainOverlapTimeout = yield* SupplyChainOverlapTimeoutMs;
74842
+ const supplyChainOverlapTimeout = capToDeadline(yield* SupplyChainOverlapTimeoutMs);
74519
74843
  const supplyChainFiber = yield* forkChild(shouldRunSupplyChain ? runCollect(applyPerElementPipeline(supplyChainService.run({
74520
74844
  rootDirectory: scanDirectory,
74521
- userConfig: resolvedConfig.config
74845
+ userConfig: resolvedConfig.config,
74846
+ timeoutMs: supplyChainOverlapTimeout
74522
74847
  }))).pipe(map$3((diagnostics) => ({
74523
74848
  diagnostics,
74524
74849
  timedOut: false
@@ -74542,46 +74867,49 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74542
74867
  const scanConcurrency = resolveScanConcurrency(yield* OxlintConcurrency);
74543
74868
  const lintPhaseTimeoutMs = yield* LintPhaseTimeoutMs;
74544
74869
  const deadCodePhaseTimeoutMs = yield* DeadCodePhaseTimeoutMs;
74545
- const resolveDeadCodePhaseTimeoutMs = (scaledPhaseTimeoutMs) => deadCodePhaseTimeoutMs === 15e4 ? scaledPhaseTimeoutMs : deadCodePhaseTimeoutMs;
74546
74870
  const workerCountSuffix = scanConcurrency > 1 ? ` ${highlighter.dim(`[~${scanConcurrency} workers]`)}` : "";
74547
- const capToDeadline = (phaseTimeoutMs) => input.deadlineEpochMs === void 0 ? phaseTimeoutMs : Math.min(phaseTimeoutMs, remainingDeadlineBudgetMs(input.deadlineEpochMs));
74548
74871
  const shouldRunDeadCode = input.runDeadCode && !isDiffMode && (showWarnings || deadCodeMaySurfaceWhenWarningsHidden(resolvedConfig.config));
74549
74872
  const deadCodeOverlapMode = yield* DeadCodeOverlap;
74550
74873
  const shouldOverlapDeadCode = shouldRunDeadCode && deadCodeOverlapMode === "on";
74551
74874
  const deadCodeParseConcurrency = shouldOverlapDeadCode ? Math.max(1, Math.floor(scanConcurrency * DEAD_CODE_OVERLAP_PARSE_SHARE)) : void 0;
74552
74875
  const lintConcurrency = deadCodeParseConcurrency === void 0 ? scanConcurrency : Math.max(1, scanConcurrency - deadCodeParseConcurrency);
74553
- const buildCollectDeadCode = (deadCodeTimeout) => runCollect(applyPerElementPipeline(deadCodeService.run({
74554
- rootDirectory: scanDirectory,
74555
- parseConcurrency: deadCodeParseConcurrency,
74556
- workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
74557
- onCacheOutcome: (didHitCache) => {
74558
- deadCodeCacheHit = didHitCache;
74559
- },
74560
- onSummaryCacheStats: (stats) => {
74561
- deadCodeSummaryCacheHits = stats.hits;
74562
- deadCodeSummaryCacheMisses = stats.misses;
74563
- }
74564
- }).pipe(catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
74565
- yield* set(deadCodeFailure, {
74566
- didFail: true,
74567
- reason: error.message
74568
- });
74569
- return empty$4;
74570
- })))))).pipe(timeoutOption(deadCodeTimeout.phaseTimeoutMs), flatMap$2(match$3({
74571
- onNone: () => set(deadCodeFailure, {
74572
- didFail: true,
74573
- reason: `Dead-code analysis exceeded ${Math.round(deadCodeTimeout.phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
74574
- }).pipe(as([])),
74575
- onSome: succeed$2
74576
- })));
74876
+ const buildCollectDeadCode = (deadCodeTimeout) => {
74877
+ const collectDeadCode = runCollect(applyPerElementPipeline(deadCodeService.run({
74878
+ rootDirectory: scanDirectory,
74879
+ parseConcurrency: deadCodeParseConcurrency,
74880
+ workerTimeoutMs: deadCodeTimeout.workerTimeoutMs,
74881
+ onCacheOutcome: (didHitCache) => {
74882
+ deadCodeCacheHit = didHitCache;
74883
+ },
74884
+ onSummaryCacheStats: (stats) => {
74885
+ deadCodeSummaryCacheHits = stats.hits;
74886
+ deadCodeSummaryCacheMisses = stats.misses;
74887
+ }
74888
+ }).pipe(filter((diagnostic) => input.retainExcludedProjectDeadCodeDiagnostics === true || !isExcludedProjectDiagnostic(diagnostic)), catchTag("ReactDoctorError", (error) => unwrap(gen(function* () {
74889
+ yield* set(deadCodeFailure, {
74890
+ didFail: true,
74891
+ reason: error.message
74892
+ });
74893
+ return empty$4;
74894
+ }))))));
74895
+ const phaseTimeoutMs = deadCodeTimeout.phaseTimeoutMs;
74896
+ if (phaseTimeoutMs === null) return collectDeadCode;
74897
+ return collectDeadCode.pipe(timeoutOption(phaseTimeoutMs), flatMap$2(match$3({
74898
+ onNone: () => set(deadCodeFailure, {
74899
+ didFail: true,
74900
+ reason: `Dead-code analysis exceeded ${Math.round(phaseTimeoutMs / MILLISECONDS_PER_SECOND)}s and was skipped.`
74901
+ }).pipe(as([])),
74902
+ onSome: succeed$2
74903
+ })));
74904
+ };
74577
74905
  const overlapDeadCodeTimeout = resolveDeadCodeTimeout({
74578
- sourceFileCount: project.sourceFileCount,
74906
+ sourceFileCount: deadCodeSourceFileCount,
74579
74907
  deadCodeConcurrency: deadCodeParseConcurrency ?? scanConcurrency,
74580
74908
  fullConcurrency: scanConcurrency
74581
74909
  });
74582
74910
  const deadCodeFiber = shouldOverlapDeadCode ? yield* forkChild(buildCollectDeadCode({
74583
74911
  workerTimeoutMs: overlapDeadCodeTimeout.workerTimeoutMs,
74584
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(overlapDeadCodeTimeout.phaseTimeoutMs))
74912
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
74585
74913
  })) : null;
74586
74914
  const scanProgress = yield* progressService.start("Scanning...");
74587
74915
  const scanStartTime = Date.now();
@@ -74632,7 +74960,8 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74632
74960
  });
74633
74961
  return empty$4;
74634
74962
  }))));
74635
- const lintCollected = dedupeRelatedDiagnostics(yield* runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream)).pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
74963
+ const collectLintDiagnostics = runCollect(filterPerElementPipeline(shouldOverlapDeadCode ? baseLintStream.pipe(provideService(OxlintConcurrency, lintConcurrency)) : baseLintStream));
74964
+ const lintCollected = dedupeRelatedDiagnostics(yield* lintPhaseTimeoutMs === null ? collectLintDiagnostics : collectLintDiagnostics.pipe(timeoutOption(lintPhaseTimeoutMs), flatMap$2(match$3({
74636
74965
  onNone: () => set(lintFailure, {
74637
74966
  didFail: true,
74638
74967
  reason: `Lint analysis exceeded ${lintPhaseTimeoutMs / MILLISECONDS_PER_SECOND}s and was skipped.`,
@@ -74662,13 +74991,13 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74662
74991
  } else {
74663
74992
  yield* scanProgress.update(`Scanned ${scannedFilesLabel}, analyzing dead code...`);
74664
74993
  const sequentialDeadCodeTimeout = resolveDeadCodeTimeout({
74665
- sourceFileCount: totalFileCount,
74994
+ sourceFileCount: input.retainExcludedProjectDeadCodeDiagnostics === true ? deadCodeSourceFileCount : totalFileCount,
74666
74995
  deadCodeConcurrency: scanConcurrency,
74667
74996
  fullConcurrency: scanConcurrency
74668
74997
  });
74669
74998
  deadCodeCollected = deadCodeFiber !== null ? yield* join(deadCodeFiber) : yield* buildCollectDeadCode({
74670
74999
  workerTimeoutMs: sequentialDeadCodeTimeout.workerTimeoutMs,
74671
- phaseTimeoutMs: capToDeadline(resolveDeadCodePhaseTimeoutMs(sequentialDeadCodeTimeout.phaseTimeoutMs))
75000
+ phaseTimeoutMs: capOptionalToDeadline(deadCodePhaseTimeoutMs)
74672
75001
  });
74673
75002
  }
74674
75003
  const deadCodeFailureState = lintFailureState.didFail ? {
@@ -74712,7 +75041,11 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74712
75041
  metadata: scoreMetadata
74713
75042
  });
74714
75043
  const lintPartialFailures = yield* get$2(partialFailuresRef);
74715
- const securityScanFailed = yield* get$2(securityScanFailedRef);
75044
+ const didSecurityScanFail = yield* get$2(securityScanFailedRef);
75045
+ const securityScanFailed = didSecurityScanFail || didSecurityScanReachDeadline;
75046
+ let securityScanFailureReason = null;
75047
+ if (didSecurityScanReachDeadline) securityScanFailureReason = "Security scan reached the max scan duration; findings collected before the deadline were preserved.";
75048
+ else if (didSecurityScanFail) securityScanFailureReason = "Security scan failed and was skipped.";
74716
75049
  return {
74717
75050
  project,
74718
75051
  userConfig: resolvedConfig.config,
@@ -74735,6 +75068,7 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74735
75068
  scanConcurrency,
74736
75069
  supplyChainOverlapTimedOut: supplyChainResult.timedOut,
74737
75070
  securityScanFailed,
75071
+ securityScanFailureReason,
74738
75072
  lintCacheHitFileCount,
74739
75073
  lintCacheTotalFileCount,
74740
75074
  lintSidecarReplayedFileCount,
@@ -74750,7 +75084,10 @@ const runInspect = (input, hooks = {}) => gen(function* () {
74750
75084
  "inspect.runDeadCode": input.runDeadCode,
74751
75085
  "inspect.isCi": input.isCi,
74752
75086
  "inspect.scoreSurface": input.scoreSurface ?? "score"
74753
- } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })))));
75087
+ } }), (scanProgram) => flatMap$2(ScanDeadlineMs, (scanDeadlineMs) => {
75088
+ if (scanDeadlineMs === null) return scanProgram;
75089
+ return scanProgram.pipe(timeout(scanDeadlineMs), catchTag$1("TimeoutError", () => new ReactDoctorError({ reason: new ScanDeadlineExceeded({ detail: `${scanDeadlineMs / MILLISECONDS_PER_SECOND}s elapsed` }) })));
75090
+ }));
74754
75091
  const parseNodeVersion = (versionString) => {
74755
75092
  const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
74756
75093
  return {
@@ -74851,16 +75188,6 @@ const resolveNodeForOxlint = () => {
74851
75188
  });
74852
75189
  });
74853
75190
  /**
74854
- * Zip-Slip defense: relative paths come from git (`diff --name-only`), which
74855
- * normalizes during ordinary adds, but a crafted index/pack/symlinked tree can
74856
- * smuggle `..` segments that escape the temp root. Resolve against the temp dir
74857
- * and reject anything that lands outside before writing.
74858
- */
74859
- const isPathInsideDirectory = (childAbsolutePath, parentAbsolutePath) => {
74860
- const relative = path.relative(parentAbsolutePath, childAbsolutePath);
74861
- return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
74862
- };
74863
- /**
74864
75191
  * Writes a set of source files (supplied by `readContent` — e.g.
74865
75192
  * `git show <ref>:<path>` for a baseline tree, or `git show :<path>` for the
74866
75193
  * index) into a temp tree that mirrors the project layout, copying the
@@ -75135,6 +75462,7 @@ const buildJsonReport = (input) => {
75135
75462
  directory: input.directory,
75136
75463
  diff: toJsonDiff(input.diff),
75137
75464
  projects,
75465
+ ...input.skippedProjects && input.skippedProjects.length > 0 ? { skippedProjects: [...input.skippedProjects] } : {},
75138
75466
  diagnostics: flattenedDiagnostics,
75139
75467
  summary,
75140
75468
  elapsedMilliseconds: input.totalElapsedMilliseconds,
@@ -75159,7 +75487,7 @@ const buildSkippedChecks = (input) => {
75159
75487
  else if (input.lintPartialFailures.length > 0) skippedCheckReasons["lint:partial"] = input.lintPartialFailures.join("; ");
75160
75488
  if (input.didDeadCodeFail && input.deadCodeFailureReason !== null) skippedCheckReasons["dead-code"] = input.deadCodeFailureReason;
75161
75489
  if (input.supplyChainOverlapTimedOut) skippedCheckReasons["supply-chain"] = "Supply-chain analysis timed out and was skipped.";
75162
- if (input.securityScanFailed) skippedCheckReasons["security-scan"] = "Security scan failed and was skipped.";
75490
+ if (input.securityScanFailed) skippedCheckReasons["security-scan"] = input.securityScanFailureReason ?? "Security scan failed and was skipped.";
75163
75491
  return {
75164
75492
  skippedChecks,
75165
75493
  skippedCheckReasons
@@ -75194,14 +75522,6 @@ const getDiffInfo = (directory, explicitBaseBranch, includeUntracked) => runProm
75194
75522
  };
75195
75523
  }).pipe(provide(Git.layerNode)));
75196
75524
  const filterSourceFiles = (filePaths) => filePaths.filter(isLintableSourceFile);
75197
- /**
75198
- * Back-compat alias: the streaming pipeline holds its caches in
75199
- * per-pipeline closures that are garbage-collected when the pipeline
75200
- * goes out of scope, so there is nothing to clear at module scope. The
75201
- * public CLI's `clearCaches()` still calls this for symmetry with the
75202
- * other `clear*` helpers.
75203
- */
75204
- const clearAutoSuppressionCaches = () => {};
75205
75525
  const createLintPhaseAbortError = () => new ReactDoctorError({ reason: new OxlintSpawnFailed({ cause: "lint phase aborted" }) });
75206
75526
  const createOxlintSpawnSlots = (concurrency) => createWorkerSlots({
75207
75527
  slotCount: resolveScanConcurrency(concurrency),
@@ -75214,59 +75534,34 @@ const createOxlintSpawnSlots = (concurrency) => createWorkerSlots({
75214
75534
  * untouched so the loader sees the same plain object either way.
75215
75535
  */
75216
75536
  const defineConfig = (config) => config;
75217
- const AI_TRAINING_ENV_VARS = [
75218
- ["HF_DATASETS_CACHE", "huggingface"],
75219
- ["HF_HOME", "huggingface"],
75220
- ["HUGGINGFACE_HUB_CACHE", "huggingface"],
75221
- ["SPACE_ID", "huggingface-spaces"],
75222
- ["CUDA_VISIBLE_DEVICES", "cuda"],
75223
- ["NVIDIA_VISIBLE_DEVICES", "nvidia"],
75224
- ["ROCR_VISIBLE_DEVICES", "rocm"],
75225
- ["TPU_NAME", "google-tpu"],
75537
+ const AI_TRAINING_BY_ENVIRONMENT_VARIABLE = [
75226
75538
  ["WANDB_RUN_ID", "wandb"],
75227
75539
  ["WANDB_SWEEP_ID", "wandb"],
75228
75540
  ["MLFLOW_RUN_ID", "mlflow"],
75229
- ["MLFLOW_TRACKING_URI", "mlflow"],
75230
75541
  ["COMET_EXPERIMENT_KEY", "comet"],
75231
75542
  ["NEPTUNE_RUN_ID", "neptune"],
75232
75543
  ["CLEARML_TASK_ID", "clearml"],
75233
75544
  ["DVC_STAGE", "dvc"],
75234
75545
  ["RAY_WORKER_PROCESS", "ray"],
75235
- ["RAY_ADDRESS", "ray"],
75236
- ["MUJOCO_GL", "mujoco"],
75237
- ["MUJOCO_PATH", "mujoco"],
75238
- ["GYM_DISABLE_ENV_CHECKER", "gymnasium"],
75239
75546
  ["SM_TRAINING_ENV", "sagemaker"],
75240
75547
  ["TRAINING_JOB_ARN", "sagemaker"],
75241
- ["SAGEMAKER_BASE_DIR", "sagemaker"],
75242
75548
  ["AZUREML_RUN_ID", "azure-ml"],
75243
- ["AZURE_ML_MODEL_DIR", "azure-ml"],
75244
- ["CLOUD_ML_PROJECT_ID", "vertex-ai"],
75245
- ["VERTEX_AI_LOG_LEVEL", "vertex-ai"],
75246
- ["DET_MASTER", "determined-ai"],
75247
- ["LIGHTNING_USER_ID", "lightning-ai"],
75248
75549
  ["FLYTE_INTERNAL_EXECUTION_ID", "flyte"],
75249
- ["ARGO_WORKFLOW_NAME", "argo-workflows"],
75250
75550
  ["KFP_POD_NAME", "kubeflow-pipelines"],
75251
75551
  ["KAGGLE_KERNEL_RUN_TYPE", "kaggle"],
75252
75552
  ["COLAB_BACKEND_VERSION", "google-colab"],
75253
- ["DATABRICKS_RUNTIME_VERSION", "databricks"],
75254
75553
  ["DAYTONA_WS_ID", "daytona"],
75255
- ["DAYTONA_WS_NAME", "daytona"],
75256
75554
  ["E2B_SANDBOX_ID", "e2b"],
75257
75555
  ["MODAL_FUNCTION_ID", "modal"],
75258
75556
  ["MODAL_TASK_ID", "modal"],
75259
75557
  ["RUNPOD_POD_ID", "runpod"],
75260
- ["REPLICATE_USERNAME", "replicate"],
75261
75558
  ["VAST_CONTAINERLABEL", "vast-ai"],
75262
- ["HARBOR_URL", "harbor"],
75263
- ["HARBOR_HOSTNAME", "harbor"],
75264
75559
  ["SWE_BENCH_TASK", "swe-bench"],
75265
75560
  ["SWEBENCH_TASK", "swe-bench"],
75266
75561
  ["SWE_AGENT_MODEL", "swe-agent"]
75267
75562
  ];
75268
- const detectAiTrainingEnvironment = () => {
75269
- for (const [envVar, label] of AI_TRAINING_ENV_VARS) if (process.env[envVar]) return label;
75563
+ const detectAiTrainingEnvironment = (environment = process.env) => {
75564
+ for (const [environmentVariable, label] of AI_TRAINING_BY_ENVIRONMENT_VARIABLE) if (environment[environmentVariable]?.trim()) return label;
75270
75565
  return null;
75271
75566
  };
75272
75567
  const unionValues = (baseValues, overrideValues) => [...new Set([...baseValues, ...overrideValues])];
@@ -75460,7 +75755,6 @@ const clearCaches = () => {
75460
75755
  clearPackageJsonCache();
75461
75756
  clearIgnorePatternsCache();
75462
75757
  clearPackageRoleCache();
75463
- clearAutoSuppressionCaches();
75464
75758
  clearMinifiedFileCache();
75465
75759
  };
75466
75760
  const toJsonReport = (result, options) => buildJsonReport({
@@ -75487,4 +75781,4 @@ const toJsonReport = (result, options) => buildJsonReport({
75487
75781
  export { AmbiguousProjectError, NoReactDependencyError, NotADirectoryError, PackageJsonNotFoundError, ProjectNotFoundError, ReactDoctorError, buildJsonReport, buildJsonReportError, clearCaches, defineConfig, diagnose, filterSourceFiles, getDiffInfo, hasReactRuntime, isProjectDiscoveryError, isReactDoctorError, summarizeDiagnostics, toJsonReport };
75488
75782
 
75489
75783
  //# sourceMappingURL=index.js.map
75490
- //# debugId=0c9da9c7-4847-5807-983e-a57b7eec97cd
75784
+ //# debugId=e18e3c42-e18e-5087-9b71-eb1bcb203d6b